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:
| Property | Meaning |
|---|---|
status | The HTTP status; undefined when no response came back. |
code | The problem's error code, such as rate_limited. |
problem | The whole problem body, typed: detail, errors[] and the rest. |
requestId | The Request-Id. Quote it to support. |
headers | The response headers. |
The subclass says what kind of failure it is:
| Class | Statuses | Typical cause |
|---|---|---|
ValidationError | 400, 413, 415, 422, 428 | A bad parameter or body; problem.errors names each field. |
AuthenticationError | 401 | No key, or one that cannot be accepted. |
PermissionError | 403 | The key lacks the scope, or the operation needs test mode. |
NotFoundError | 404 | No such object, or not one this key can see. |
ConflictError | 409, 412 | A state conflict, or a stale If-Match. |
RateLimitError | 429 | Over budget. Carries retryAfterMs. |
ApiError | any other status | 500, 503, 405, 406, or a body that is not a problem (a proxy's HTML page). |
ConnectionError | no response | DNS, TLS or a dropped connection. |
TimeoutError | no response | An attempt took longer than timeout. A kind of ConnectionError. |
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 any5xx(503 overloadedincluded); - 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
- 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.retryAfterMssays how long). - 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.
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.signalcancels 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.