> Agent-readable docs index: /llms.txt. Download /docs.zip to grep all markdown files locally.

---
title: "Handling errors"
---

Orders can fail to place for a variety of different reasons. In this guide, we'll walk through how to handle various failure modes. Some examples of issues which can occur while checking out are:

* **Deliverability**. Most merchants only deliver to certain parts of the world. If your buyer's identity is outside of the regions where the merchant ships, then we won't be able to fulfill your order.
* **External service failures**. The Rye API builds on top of many other systems. Occassionally one of these services will fail, and this bubbles up through our API.
* **Input validation**. Rye reflects the validation requirements of the store(s) your cart holds products from. Some merchants require that the second address line is provided, for instance.
* **Merchant configuration**. Rye attempts to support any store on the internet, but some platforms—notably Shopify—are extremely customizable and not all setups are supported by us.
* **Payment errors**. Payment processing may fail for reasons such as insufficient funds, unrecognized payment methods, or declined transactions.
* **Product inventory**. Inventory availability is verified before checkout, but items in high demand can go out of stock during order placement.

## Overview

Placing an order consists of two phases.

1. Cart submission (synchronous). When you make a request to [`submitCart`](/api-reference/submitcart) we run some checks against the [`Cart`](/api-reference/cart) to try and verify whether it can be successfully purchased, and if you passed us a payment method we'll charge it before moving to the next phase.
2. Order placement (asynchronous). After [`submitCart`](/api-reference/submitcart) returns, your order is placed on a queue and is processed asynchronously. You must poll the [`checkoutByCartID`](/api-reference/checkoutbycartid) query for updates. Sometimes things go wrong at this stage and you'll need to replace the order.

If either of these phases fail, our system will try to determine the retryability of the operation that failed. Cart submission returns this information via a GraphQL extension field named `retryable`, and order placement attaches this information to the [`OrderFailedOrderEvent`](/api-reference/orderfailedorderevent) object. In both cases, `retryable` is an optional boolean field which you can interpret like so:

* A `true` value means our system believes the error is intermittent, and you can safely retry the operation.
* A `false` value means the failure either isn't intermittent (and thus won't be fixed through retries), or that retrying the operation is unsafe (e.g. there is a risk of charging twice).
* A missing value means our system didn't have enough information to make a judgement about retryability. You should investigate these cases yourself.

## Guidance by phase

### Cart submission (synchronous)

Cart submission is a synchronous process where Rye verifies that it is possible for us to place your order. Our systems perform tasks like address validation and product inventory checks to make sure that the order will go through if we promote it to the order placement phase.

Most errors occurring in this phase are safe to retry. The very last step taken by our systems in this phase is to capture payment from the provided payment token (if one is present), and immediately after that the cart gets sent to the next phase.

If you do not receive a `retryable` extension with value `true`, then that generally means the cart submission error needs to be fixed by either altering your request or coordinating with the merchant. For instance, the following response indicates that cart submission cannot be retried due to how the merchant has configured their store:

```json
{
  "data": {
    "submitCart": {
      "cart": {
        // ...
        "stores": [{
          // ...
          "errors": [{
            "code": "FEATURE_UNSUPPORTED",
            "message": "The merchant does not support promo codes."
          }]
        }]
      },
      "errors": []
    }
  },
  "extensions": {
    "retryable": false
  }
}
```

Here are some example failures:

* A **payment error** (`PAYMENT_FAILED`) usually requires a change of payment method to resolve. You could prompt your shopper to key in a different credit card. Because we don't expect payment method errors to solve themselves with a retry, we'll send back `retryable: false`.
* A **deliverability error** (`UNDELIVERABLE`) means that the merchant can't ship the order to the address in the buyer identity. You could prompt your shopper to enter a different delivery address. We'll send back `retryable: false` in this case.
* A **product inventory error** (`PRODUCTS_UNAVAILABLE`) can *sometimes* resolve itself if the merchant happens to restock the product in between cart submission attempts. We'll send back `retryable: true` in this case.
* An **upstream service error** (`UPSTREAM_SERVICE_ERROR`) is generally transient, and we'll usually send back `retryable: true`.

#### Caveat: Request timeouts

Calling [`submitCart`](/api-reference/submitcart) involves making a synchronous request to our API. If you have configured a low request timeout—or our systems exceed our own internal timeout—you'll get back an HTTP 503 or HTTP 504 status code. In this case, it is entirely possible that your cart ended up successfully moving to the order placement phase in the background.

For this reason, you should **not** immediately retry request timeouts to [`submitCart`](/api-reference/submitcart). You must poll the [`checkoutByCartID`](/api-reference/checkoutbycartid) query to verify what ended up happening to the cart after your request timed out. See the next section for specific guidance on this.

### Order placement (asynchronous)

After cart submission completes, the Rye API attempts to actually pay for the items inside the cart. We do this by creating orders with the merchant(s) that sell the items you're trying to purchase. If you're purchasing two items that are each sold by different merchants, then we'll end up creating two orders in total from your cart.

Most orders which get to this point will ultimately be successful, but success is not guaranteed. As Rye needs to talk to external systems to place the order, this phase is particularly vulnerable to failures caused by upstream service outages.

You should poll the [`checkoutByCartID`](/api-reference/checkoutbycartid) query for updates on your order. Progress on each order is communicated via [`OrderEvent`](/api-reference/orderevent) objects stored against each [`Order`](/api-reference/order) object on the [`Checkout`](/api-reference/checkout).

At a high level, the lifecycle of a single order looks like this:

<Mermaid
  chart="flowchart TD
  submitCart --> OrderSubmissionStartedOrderEvent

  subgraph Order placement
    OrderSubmissionStartedOrderEvent -->|success| OrderSubmissionSucceededOrderEvent
    OrderSubmissionSucceededOrderEvent --> OrderPlacedOrderEvent

    OrderSubmissionStartedOrderEvent -->|failure| OrderFailedOrderEvent
  end"
/>

You can poll for updates using the following query:

```graphql
query PollForCheckoutStatus($cartId: ID!) {
  checkoutByCartID(id: $cartId) {
    orders {
      events {
        __typename
        ... on OrderFailedOrderEvent {
          reason
          reasonCode
          retryable
        }
      }
    }
  }
}
```

Here's an example payload showing an order that failed with a retryable error:

```json
{
  "data": {
    "checkoutByCartID": {
      "orders": [{
        "events": [{
          "__typename": "OrderSubmissionStartedOrderEvent"
        }, {
          "__typename": "OrderFailedOrderEvent",
          "reason": "An upstream provider is experiencing issues. Please try again later.",
          "reasonCode": "UPSTREAM_SERVICE_ERROR",
          "retryable": true
        }]
      }]
    }
  }
}
```

There are a few cases you should look out for here:

1. The order never starts processing. This could be the case if your call to [`submitCart`](/api-reference/submitcart) timed out, and something prevented our system from moving it to the order placement phase. You can detect this by the `events` list being empty.
   * We recommend polling for 5 minutes before declaring that the order didn't start processing.
   * In this case, it is safe to call [`submitCart`](/api-reference/submitcart) on the original cart to retry submission.
2. The order gets stuck during processing. You can detect this by the `events` list containing only a single event of type [`OrderSubmissionStartedOrderEvent`](/api-reference/ordersubmissionstartedorderevent).
   * We recommend polling for 10 minutes before handling this edge case.
   * This case is rare and indicates something went wrong on our side
   * You should *not* automatically attempt to replace orders which fall into this bucket. Contact us with the cart ID so we can help you debug the issue.
3. The order fails to place. You can detect this by the presence of an [`OrderFailedOrderEvent`](/api-reference/orderfailedorderevent) event inside the `events` list.
   * You can use the [`OrderFailedOrderEvent.retryable`](/api-reference/orderfailedorderevent) field to determine whether you can automatically attempt to replace the order. The value of this field has the same meaning as the `retryable` extension.

In cases (2) and (3), it is not possible to retry the order by calling [`submitCart`](/api-reference/submitcart) on the original [`Cart`](/api-reference/cart). Our systems prevent this, and you will receive a non-retryable `ALREADY_SUBMITTED` error code from the [`submitCart`](/api-reference/submitcart) operation.

Instead, you must [create a new cart](/get-started/cart-management/create-a-cart) containing the item(s) from the order(s) that failed to place, and then attempt to submit the new [`Cart`](/api-reference/cart).

## Refunds

Rye [automatically issues refunds](/refunds) to the payment method token passed to [`submitCart`](/api-reference/submitcart) when order placement fails. If you are an enterprise customer using invoice billing (and therefore do not pass us a payment method token), then the failed order will not be charged on your next invoice.

If you are an enterprise customer using invoice billing, then you may need to refund your shopper depending on how your app is structured.

## Sample code

The below is an example of how you can implement handling ordering errors with retries in TypeScript. We use the [`async-retry`](https://www.npmjs.com/package/async-retry) NPM package to implement retries for the [`submitCart`](/api-reference/submitcart) operation using exponential backoff.

<Tabs items={["example.ts", "helpers.ts"]}>
  <Tab title="example.ts">
    ```typescript
    import retry from 'async-retry';
    import { checkoutByCartId, createCart, submitCart, sleep } from './helpers';

    const TEN_MINUTES_IN_MS = 1_000 * 60 * 10;

    async function createAndSubmitCartWithRetries(items, remainingRetries = 1) {
      const cartId = await createCart(items);
      const failedProductIds = [];

      const { result: submitResult, status: submitStatus } = await retry(async () => {
        const { result, status } = await submitCart(cartId);
        if (result.extensions.retryable) {
          throw new Error('Retry retryable error');
        }

        return { result, status };
      }, { retries: 3 });

      const didSubmitTimeout = submitStatus === 503 || submitStatus === 504;

      if (!didSubmitTimeout) {
        if (submitResult.errors.length) {
          return { didSubmit: false, failedProductIds };
        }

        for (const store of submitResult.cart.stores) {
          if (store.errors.length) {
            return { didSubmit: false, failedProductIds };
          }
        }
      }

      const endPollingBy = Date.now() + TEN_MINUTES_IN_MS;
      const lineItemsByOrderId = new Map();
      const processedOrderIds = new Set();
      const retryableOrderIds = new Set();

      while (Date.now() < endPollingBy) {
        const pollResult = await checkoutByCartId(cartId);
        for (const order of pollResult.data.orders) {
          lineItemsByOrderId.set(order.id, order.lineItems);
        }

        const didProcessAllOrders = processedOrderIds.size === pollResult.data.checkoutByCartID.orders.length;
        if (didProcessAllOrders) {
          break;
        }

        for (const order of pollResult.data.orders) {
          if (processedOrderIds.has(order.id)) {
            // Don't process the same order twice
            continue;
          }

          const orderFailedEvent = order.events.find((event) => (
            event.__typename === 'OrderFailedOrderEvent',
          ));
          const orderSucceededEvent = order.events.find((event) => (
            event.__typename === 'OrderSucceededOrderEvent',
          ));

          if (!orderFailedEvent && !orderSucceededEvent) {
            // Nothing to do yet
            continue;
          }

          if (orderSucceededEvent) {
            // Nothing to do -- all went OK
            processedOrderIds.add(order.id);
          } else if (orderFailedEvent) {
            // Track items belonging to this order so we can either retry with a new
            // cart or communicate with the user later.
            processedOrderIds.add(order.id);

            if (orderFailedEvent.retryable) {
              retryableOrderIds.add(order.id);
            } else {
              // Track failed products we cannot retry
              if (lineItem.__typename === 'AmazonLineItem') {
                failedProductIds.push({
                  marketplace: 'AMAZON',
                  productId: lineItem.productId,
                });
              } else {
                failedProductIds.push({
                  marketplace: 'SHOPIFY',
                  variantId: lineItem.variantId,
                });
              }
            }
          } else {
            // No info about this order; chill.
          }
        }

        // Sleep between polls to be a good citizen
        await sleep(200);
      }

      for (const [orderId, lineItems] of lineItemsByOrderId.entries()) {
        if (!processedOrderIds.has(orderId)) {
          // This order got stuck. We'll add it to our list of retryable order IDs.
          retryableOrderIds.add(orderId);
        }
      }

      // Do we have retryable order IDs and remaining retries?
      if (retryableOrderIds.size && remainingRetries > 0) {
        const retryableCartItems = {
          amazonCartItemsInput: [],
          shopifyCartItemsInput: [],
        };

        // Assemble cart items for new cart
        for (const [orderId, lineItems] of lineItemsByOrderId.entries()) {
          if (!retryableOrderIds.has(orderId)) continue;

          for (const lineItem of lineItems) {
            if (lineItem.__typename === 'AmazonLineItem') {
              retryableCartItems.amazonCartItemsInput.push({
                productId: lineItem.productId,
                quantity: lineItem.quantity,
              });
            } else {
              retryableCartItems.shopifyCartItemsInput.push({
                quantity: lineItem.quantity,
                variantId: lineItem.variantId,
              });
            }
          }
        }

        // Retry products w/ a new cart
        const { failedProductIds: failedRetriedProductIds } = await createAndSubmitCartWithRetries(
          retryableCartItems,
          remainingRetries - 1,
        );

        failedProductIds.push(...failedRetriedProductIds);
      }

      return { didSubmit: true, failedProductIds };
    }

    const result = await createAndSubmitCartWithRetries({
      shopifyCartItemsInput: [{
        quantity: 1,
        variantId: '44346795295022',
      }],
    });

    if (!result.didSubmit) {
      // TODO: Inform user checkout did not even start (e.g. payment error).
    } else if (result.failedProductIds.length) {
      // TODO: Inform user about failed checkout for some products
    } else {
      // TODO: Inform user about checkout success
    }
    ```
  </Tab>

  <Tab title="helpers.ts">
    ```typescript
    async function request(body) {
      const result = await fetch(process.env.RYE_API_GRAPHQL_URL, {
        headers: {
          Authorization: process.env.RYE_API_AUTH_HEADER,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(body),
      });

      const json = await result.json();
      return { response: json, status: result.status };
    }

    export async function sleep(milliseconds) {
      return new Promise((resolve) => setTimeout(resolve, milliseconds));
    }

    export async function createCart(items) {
      const input = {
        items,
        buyerIdentity: {
          firstName: 'John',
          lastName: 'Doe',
          email: 'john@rye.com',

          address1: '1460 Broadway',
          city: 'New York City',
          provinceCode: 'NY',
          countryCode: 'US',
          postalCode: '10036',
        },
      };

      const result = await request({
        query: `
          mutation CreateCart($input: CartCreateInput!) {
            createCart(input: $input) {
              cart {
                id
              }
            }
          }
        `,
        variables: { input },
      });

      // TODO: You would want to handle cart creation errors!

      return result.response.data.createCart.cart.id;
    }

    export async function submitCart(cartId) {
      return request({
        query: `
          mutation SubmitCart($input: CartSubmitInput!) {
            submitCart(input: $input) {
              cart {
                stores {
                  errors {
                    code
                    message
                  }
                }
              }
              errors {
                code
                message
              }
            }
          }
        `,
        // TODO: You may need to pass a payment token here, depending on how your billing is configured.
        variables: { input: { id: cartId } },
      })
    }

    export async function checkoutByCartId(cartId) {
      return request({
        query: `
          query CheckoutByCartId($cartId: ID!) {
            checkoutByCartID(cartID: $cartId) {
              orders {
                id
                events {
                  __typename
                  ... on OrderFailedOrderEvent {
                    reason
                    reasonCode
                    retryable
                  }
                }
                lineItems {
                  __typename
                  ... on AmazonLineItem {
                    productId
                    quantity
                  }
                  ... on ShopifyLineItem {
                    quantity
                    variantId
                  }
                }
              }
            }
          }
        `,
        variables: { cartId },
      });
    }
    ```
  </Tab>
</Tabs>


---

*Powered by [holocron.so](https://holocron.so)*
