> ## Documentation Index
> Fetch the complete documentation index at: https://docs.stateset.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Error Handling & Retries

> Handle expected commerce rejections, preserve unexpected errors, and decide when a request can be retried without duplicating work.

Start with the contract for the interface you call. Embedded Node errors, ResponseCX HTTP
responses, and other engines expose different fields. An error handler needs both the
failure code and the operation's replay behavior to choose a useful next action.

## Read the right error shape

| Interface | Where to look | Example |
| - | - | - |
| Embedded Node 1.35.1 | Thrown error's `code`, `message`, and optional `details` | `error.code === 'INSUFFICIENT_STOCK'` |
| ResponseCX HTTP | HTTP status and top-level response `code`; validation can include `issues` | `body.code === 'idempotency_in_progress'` |
| Commerce HTTP | Nested `error.code` and optional `error.invariant` | Branch on the documented invariant when present |
| Other engines | That service's error envelope | See [errors across the engines](/api-reference/errors-across-engines) |

The published Node binding unwraps native errors into JavaScript errors. `details.httpStatus`
is metadata on an engine failure; a local call did not make an HTTP request. Argument-conversion
errors can retain a native binding code instead. Do not assume every thrown value has a known
commerce code, and do not parse an error message as a universal JSON response.

## Turn an expected rejection into an actionable result

The example below handles one operation-specific failure: a manual stock reservation that
cannot be satisfied. It returns an out-of-stock result for that case and propagates every
other error. It never retries a reservation automatically.

Use Node.js **20.20.0+**, npm **10+**, and **`@stateset/embedded` 1.35.1**:

```bash theme={null}
mkdir stateset-error-example
cd stateset-error-example
npm init -y
npm install @stateset/embedded@1.35.1
```

Save this as `error-handling.test.mjs`:

```javascript theme={null}
import test from 'node:test';
import assert from 'node:assert/strict';
import { Commerce } from '@stateset/embedded';

async function reserveForCheckout(inventory, sku, quantity, checkoutId) {
  try {
    const reservation = await inventory.reserve(
      sku, quantity, 'checkout', checkoutId, 300,
    );
    return { kind: 'reserved', reservationId: reservation.id };
  } catch (error) {
    if (error?.code !== 'INSUFFICIENT_STOCK') throw error;
    const stock = await inventory.getStock(sku);
    return {
      kind: 'out_of_stock',
      available: stock?.totalAvailable ?? null,
    };
  }
}

async function fixture() {
  const commerce = new Commerce(':memory:');
  await commerce.inventory.createItem({
    sku: 'MUG-001', name: 'Ceramic mug', initialQuantity: 2,
  });
  return commerce;
}

test('insufficient stock returns an outcome without allocating units', async () => {
  const commerce = await fixture();
  const result = await reserveForCheckout(commerce.inventory, 'MUG-001', 3, 'checkout-001');
  assert.deepEqual(result, { kind: 'out_of_stock', available: '2' });
  const stock = await commerce.inventory.getStock('MUG-001');
  assert.equal(stock.totalOnHand, '2');
  assert.equal(stock.totalAllocated, '0');
  assert.equal(stock.totalAvailable, '2');
});

test('a successful hold returns the ID needed to release it', async () => {
  const commerce = await fixture();
  const result = await reserveForCheckout(commerce.inventory, 'MUG-001', 1, 'checkout-002');
  assert.equal(result.kind, 'reserved');
  assert.ok(result.reservationId);
  assert.equal((await commerce.inventory.getStock('MUG-001')).totalAllocated, '1');
  await commerce.inventory.releaseReservation(result.reservationId);
  assert.equal((await commerce.inventory.getStock('MUG-001')).totalAvailable, '2');
});

test('an unexpected error is preserved without a retry or stock lookup', async () => {
  // A test double verifies our handler's failure path; it does not simulate engine internals.
  const failure = new Error('Synthetic unexpected failure');
  let attempts = 0;
  const inventory = {
    async reserve() { attempts++; throw failure; },
    async getStock() { assert.fail('Unexpected errors must not become stock results'); },
  };
  await assert.rejects(
    () => reserveForCheckout(inventory, 'MUG-001', 1, 'checkout-003'),
    error => error === failure,
  );
  assert.equal(attempts, 1);
});
```

```bash theme={null}
node --test error-handling.test.mjs
```

**Success:** the runner reports **3 tests, 3 passed, 0 failed**, and exits with status `0`.
The first two tests call the real engine. The third injects an unexpected error to verify that
your handler preserves it and makes exactly one attempt.

`available` is a fresh stock observation after the rejection, not a promise that those units
will remain available. A later checkout must reserve again against current stock. If the stock
lookup itself fails, that error propagates rather than inventing an availability value.

This helper creates **manual** checkout holds. Do not add it around an order workflow that
already reserves the same units; the [orders quickstart](/guides/orders-quickstart) demonstrates
order-managed reservations.

## Choose the next action by operation

| Observed failure | Useful next action | Avoid |
| - | - | - |
| Reservation returns `INSUFFICIENT_STOCK` | Offer a quantity change or wait for a stock change | Repeating the same hold in a tight loop |
| Customer creation returns `CONFLICT` for an existing email | Look up the email, then explicitly choose reuse or update | Treating every conflict on every resource as success |
| Local refund returns `VALIDATION` | Inspect payment state, amount, and existing pending refunds | Assuming every validation error is a temporary failure |
| Return completion returns `POLICY_REJECTED` | Check required dispositions and the binding's supported methods | Retrying without satisfying the policy |
| Lookup returns `null` | Show a missing-record result or verify the ID and database | Dereferencing it or silently creating a replacement |
| Unknown code or unexpected exception | Propagate it with operation context for investigation | Returning success or converting it to an unrelated business result |

These are examples from the [customer](/guides/customers-quickstart),
[payment](/guides/payments-quickstart), and [return](/guides/returns-quickstart) walkthroughs.
A code such as `VALIDATION` names a category; it does not identify every failed precondition.

## Retry HTTP requests deliberately

Before retrying, establish all three:

1. The failure is one the service describes as retryable, or a transient failure on a read.
2. Repeating the operation is safe under that endpoint's contract. A custom header does not
   make an endpoint idempotent if the server does not support it.
3. The retry has a bound: an attempt limit and an overall deadline. Stop when either is reached.

For supported write replays, persist the original key and request before sending the first
attempt. Reuse that same key and payload within the endpoint's documented replay window. Keep
any returned resource or operation ID for reconciliation. Changing the payload is an intentional
new operation, not a retry of the old one.

Use the service's retry guidance, including `Retry-After` when supplied. Schedule a retry only
if its delay fits the remaining deadline; otherwise surface the failure or defer the operation.
For retryable failures without a prescribed delay, use bounded backoff with jitter to spread
attempts across clients. Delaying a request does not make a non-replayable write safe.

### A ResponseCX conflict is not automatically success

The [agent creation contract](/api-reference/responsecx/agents-create) distinguishes:

| HTTP status and code | Meaning | Next action |
| - | - | - |
| `409 agent_limit_reached` | The organization reached its creation ceiling | Resolve the limit; repeating the same request does not remove it |
| `409 idempotency_in_progress` | An earlier request with that key is still running | Wait for it to settle before replaying the original request with its original key |
| `422 idempotency_key_reused` | That key was used with a different body | Reconcile the original intent; do not rotate keys just to force the changed write through |

Authentication failures need the correct host, header, credential, or scope. A delay does not
fix a `401` or `403`. Validation errors need a corrected request. Read the response code before
classifying an HTTP status as a retry signal.

## Reconcile uncertain outcomes

A timeout means the client did not receive a usable result. A write may still have completed.
The same caution applies to a gateway error after a downstream service accepted an operation.

For a supported replay, resend the original operation according to that endpoint's contract.
Otherwise, use its returned operation ID or documented lookup to inspect the outcome. If you
cannot establish whether it completed, retain an unresolved state for reconciliation instead
of issuing another write with a new identity.

Apply this separately to each system. A local payment idempotency key does not configure a
provider's retry behavior. A payment timeout also does not establish that you should release a
stock hold: reconcile the payment before deciding what the inventory workflow should do.

## Capture useful failure context

Record the interface and version, operation name, attempt number, elapsed time, error code,
and any request/correlation ID the service supplies. Preserve the original exception as the
cause when adding context. A database path or workspace mismatch can matter as much as the
request body.

Use a redacted diagnostic message rather than logging credentials, entire customer records,
or unrestricted response bodies. There is no universal `error.request_id` field across the
engines; the [error-envelope table](/api-reference/errors-across-engines#the-shapes) shows the
service-specific locations. The [support checklist](/support#report-an-api-problem) lists the
information needed for a reproduction.

## Next steps

* [Integration testing](/guides/api-testing): verify stored state after successes and rejections.
* [Inventory lifecycle](/stateset-icommerce/stateset-icommerce-inventory-lifecycle): understand holds, expiration, and release.
* [Payments and refunds](/guides/payments-quickstart): test replay identity and refundable-balance guards.
* [Errors across the engines](/api-reference/errors-across-engines): choose the right response parser.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.