# Errors and retries How failed requests are reported, and which requests are retried. ## Errors Failed requests reject with a `SynthientError` or one of its subclasses. Check them with `instanceof`: ```ts import { APIError } from "@synthient/synthient"; try { await client.lookupIP("8.8.8.8"); } catch (error) { if (!(error instanceof APIError)) { throw error; } switch (error.status) { case 402: console.log("out of lookup credits"); break; case 429: if (error.retryAfter === undefined) { console.log("monthly request budget spent"); } else { console.log(`rate limited, retry after ${error.retryAfter} ms`); } break; default: console.log("request", error.requestID, "failed:", error.detail || error.title); } } ``` | Class | Extends | Thrown when | | -------------------- | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `SynthientError` | `Error` | A request failed or a response was invalid, such as a body that is not a JSON object. It is the base class of the others. | | `APIError` | `SynthientError` | The API, or the storage host serving an export download, answered with a non-success HTTP status. A storage error carries only `status` and `retryAfter`. | | `APIConnectionError` | `SynthientError` | The connection failed before a response arrived or while its body was being read, for example because of a DNS, connection, or TLS failure, or a stream that dropped. The underlying error is its `cause`, except when the storage host that serves an export download fails, where it is omitted to keep presigned URLs out of errors. | | `APITimeoutError` | `APIConnectionError` | No data arrived for the client's `timeout` while waiting for a response or reading its body. It has no `cause`. | - **Names:** Each class sets `name` to its own name, such as `"APIError"`. - **Before a request:** Invalid arguments and configuration fail with a `TypeError` or `RangeError` instead, before any request is sent. - **Cancellation:** A canceled call rejects with its `AbortSignal`'s reason. See [Errors](https://docs.synthient.com/errors) for what each status means. ### APIError - `status` `number` — The HTTP status code, such as `402` when the account is out of credits. - `type` `string` — The problem type URI, such as `"https://docs.synthient.com/errors/unauthorized"`, or `""`. - `title` `string` — A short summary of the problem type, or `""`. - `detail` `string` — An explanation of this occurrence of the problem, or `""`. For a plain-text error, such as the edge's 401 for a malformed key, it is the response body. - `errors` `Record` — Validation messages by request field, set on 422 responses. It is an empty object when the response has none. - `requestID` `string` — Identifies the request when contacting Synthient support, or `""`. It comes from the `X-Request-ID` header, or the problem's `instance` when the header is absent. - `retryAfter` `number | undefined` — Milliseconds the server asked the caller to wait before retrying, or `undefined` if it did not say. The fields are read-only. - **Logging:** `message` contains only the HTTP status code and its standard description, such as `402 Payment Required`, so it is safe to log. - **Untrusted fields:** The other fields come from the server's problem details and can contain sensitive data, so inspect them explicitly. `APIError` can be constructed, for example to simulate failures in tests. ```ts constructor(init: APIErrorInit, options?: ErrorOptions); ``` - **Input:** Only `init.status` is required. `options` takes the standard `Error` options, such as `cause`. ### APIErrorInit - `status` `number` — Sets `APIError.status`. - `type?` `string | undefined` — Sets `APIError.type`. Defaults to `""`. - `title?` `string | undefined` — Sets `APIError.title`. Defaults to `""`. - `detail?` `string | undefined` — Sets `APIError.detail`. Defaults to `""`. - `errors?` `Record | undefined` — Sets `APIError.errors`. Defaults to an empty object. - `requestID?` `string | undefined` — Sets `APIError.requestID`. Defaults to `""`. - `retryAfter?` `number | undefined` — Sets `APIError.retryAfter`, in milliseconds. ## Retries and timeouts Which requests are retried: - **Account and feed requests** (`account`, the export methods, and stream handshakes) are retried on 500, 502, 503, and 504 responses, on 429 responses that carry `Retry-After`, on idle timeouts, and on connection failures with one of these codes: `ECONNRESET`, `ECONNREFUSED`, `EPIPE`, `ETIMEDOUT`, `ENOTFOUND`, `EAI_AGAIN`, `ENETUNREACH`, `EHOSTUNREACH`, `UND_ERR_SOCKET`, or `UND_ERR_CONNECT_TIMEOUT`. - **An error thrown by a custom `fetch`** is retried the same way when it, or an error in its `cause` chain, has one of these codes as its `code`. - **A failure while reading the JSON body** of an `account`, `listExports`, or `exportMeta` response is retried the same way, within the same retry budget. - **Other failures**, such as a TLS certificate error, are not retried. - **Billed lookups** (`lookupIP`, `lookupIPs`, and `lookupDomain`) and `health` are retried only on 429 responses that carry `Retry-After`, which the rate limiter returns before doing any work, so a retried lookup is never charged twice. - **A 429 without `Retry-After`** means the key's monthly request budget is spent or it has too many open streams, so it rejects without a retry. - **Once a stream is open**, and for downloads from the export storage host, nothing is retried. How retries wait: - Retries back off exponentially from one second, doubling up to 30 seconds, with 25% jitter. - The wait is never shorter than `Retry-After`. - A `Retry-After` longer than a minute rejects with the response's `APIError` instead of retrying. - Aborting the call's `signal` stops a wait. - Set the retry count with `maxRetries`. The client's `timeout` is an idle timeout, 60 seconds by default. - It bounds each wait for response headers and for the next part of a body. - It runs only while the client waits on the network, so a slow consumer does not trip it. - An idle timeout fails the attempt with an `APITimeoutError`. The `timeout` does not limit a whole call. Bound a call, including its retries, with `AbortSignal.timeout`: ```ts const info = await client.lookupIP("8.8.8.8", { signal: AbortSignal.timeout(10_000) }); console.log(info.ip); ``` When the signal fires, the call rejects with its reason, which for `AbortSignal.timeout` is a `DOMException` named `"TimeoutError"`.