FurniOpsDocs
TypeScript SDK

Errors and retries

The SDK's error classes, exactly when it retries, how long it waits, and how to cancel a call.

Error classes

A call either returns its typed result or throws. Every error is a FurniOpsError:

PropertyMeaning
statusThe HTTP status; undefined when no response came back.
codeThe problem's error code, such as rate_limited.
problemThe whole problem body, typed: detail, errors[] and the rest.
requestIdThe Request-Id. Quote it to support.
headersThe response headers.

The subclass says what kind of failure it is:

ClassStatusesTypical cause
ValidationError400, 413, 415, 422, 428A bad parameter or body; problem.errors names each field.
AuthenticationError401No key, or one that cannot be accepted.
PermissionError403The key lacks the scope, or the operation needs test mode.
NotFoundError404No such object, or not one this key can see.
ConflictError409, 412A state conflict, or a stale If-Match.
RateLimitError429Over budget. Carries retryAfterMs.
ApiErrorany other status500, 503, 405, 406, or a body that is not a problem (a proxy's HTML page).
ConnectionErrorno responseDNS, TLS or a dropped connection.
TimeoutErrorno responseAn attempt took longer than timeout. A kind of ConnectionError.
errors.ts
try {
  await furniops.products.get("prod_034XqBaL2mRLTZPXyRbaUc");
} catch (error) {
  if (error instanceof NotFoundError) {
    // 404: no such product, or not one this key may see.
  } else if (error instanceof ValidationError) {
    // 400, 413, 415, 422, 428: fix the request; problem.errors names the fields.
    for (const field of error.problem?.errors ?? []) console.log(field.path, field.code);
  } else if (error instanceof RateLimitError) {
    // 429 after the retries, or a Retry-After longer than 60 s.
    console.log("retry in", error.retryAfterMs, "ms");
  } else if (error instanceof AuthenticationError || error instanceof PermissionError) {
    // 401: no key, or a bad one. 403: the key lacks the scope, or test mode is required.
  } else if (error instanceof ConflictError) {
    // 409 or 412: a state or version conflict.
  } else if (error instanceof ConnectionError) {
    // No response at all (TimeoutError is one), after the retries.
  }
  if (error instanceof FurniOpsError) {
    // Every SDK error: quote requestId to support.
    console.error(error.status, error.code, error.requestId, error.message);
  }
}

WebhookVerificationError is a FurniOpsError too; see Webhooks.

What is retried

A call is retried, up to maxRetries times (2 by default), when an attempt:

  • answers 429, or any 5xx (503 overloaded included);
  • times out;
  • fails with no response at all.

Nothing else is retried: a 4xx other than 429 means the request itself must change.

Every attempt is the same request: the same body, the same headers and, for a POST, the same idempotency key, so a retry never does the work twice even when an earlier attempt succeeded and only its answer was lost.

How long it waits

  1. When the response sent Retry-After, the SDK waits exactly that long, if it is 60 seconds or less. Longer than that, it stops retrying and throws at once: a wait that long is yours to schedule (RateLimitError.retryAfterMs says how long).
  2. Otherwise it backs off exponentially: 0.5 s, 1 s, 2 s, 4 s, then 8 s at most, each with "equal jitter" (half fixed, half random), so many clients do not retry in step.
later.ts
try {
  await furniops.sources.createRun("src_034XqBaL2mRLTZPXyRbaUc", {}, { maxRetries: 5 });
} catch (error) {
  // Thrown after the retries, or at once when Retry-After asks for more than 60 s.
  if (error instanceof RateLimitError) scheduleLater(error.retryAfterMs ?? 60_000);
  else throw error;
}

Timeouts and cancellation

  • timeout (60 s by default) applies to each attempt, reading the body included. A call with retries can take longer than one timeout in total.
  • signal cancels the whole call: the attempt in flight, the retries and the waits between them. A cancelled call rejects with the signal's reason and is never retried.
// The whole call, retries and waits included, must finish within 10 seconds.
const recent = await furniops.products.list(
  { limit: 100 },
  { signal: AbortSignal.timeout(10_000) },
);
console.log(recent.data.length);

Disabling retries

Pass maxRetries: 0, on the client or on one call, when you run your own retry policy (a queue with its own backoff, for instance). Keep sending the same idempotencyKey on your retries.

On this page