---
title: "JavaScript SDK error classes, RetryPolicy, RequestOptions"
type: reference
tags: [javascript, typescript, sdk, errors, retries]
created: 2026-09-17
updated: 2026-09-17
confidence: high
sources:
  - raw/docs/sdk__javascript__api__classes__TypeSafeError.md
  - raw/docs/sdk__javascript__api__classes__APIError.md
  - raw/docs/sdk__javascript__api__classes__APIConnectionError.md
  - raw/docs/sdk__javascript__api__classes__APITimeoutError.md
  - raw/docs/sdk__javascript__api__classes__APIUserAbortError.md
  - raw/docs/sdk__javascript__api__classes__AuthenticationError.md
  - raw/docs/sdk__javascript__api__classes__BadRequestError.md
  - raw/docs/sdk__javascript__api__classes__PermissionDeniedError.md
  - raw/docs/sdk__javascript__api__classes__NotFoundError.md
  - raw/docs/sdk__javascript__api__classes__UnprocessableEntityError.md
  - raw/docs/sdk__javascript__api__classes__RateLimitError.md
  - raw/docs/sdk__javascript__api__classes__InternalServerError.md
  - raw/docs/sdk__javascript__api__interfaces__RetryPolicy.md
  - raw/docs/sdk__javascript__api__interfaces__RequestOptions.md
  - raw/github/typesafe-sdk-js/src/errors.ts
  - raw/github/typesafe-sdk-js/src/retry.ts
  - raw/github/typesafe-sdk-js/src/client.ts
  - raw/github/typesafe-sdk-js/src/types.ts
  - raw/github/typesafe-sdk-js/examples/demo.ts
jev_version: "jev-1.13.0"
sdk_js: "0.6.0"
summary: "Error hierarchy of @typesafe-ai/sdk 0.6.0, status-to-class mapping, RetryPolicy defaults (2 retries, 500ms/5s backoff, 0.25 jitter) and RequestOptions."
---

# JavaScript SDK error classes, RetryPolicy, RequestOptions

> **TL;DR** Every throw from `@typesafe-ai/sdk` 0.6.0 is a `TypeSafeError`. Catch `APIError` for HTTP failures (`.status`, `.headers`, `.body`, `.requestId`; `RateLimitError` adds `.retryAfterMs`), `APIConnectionError` for transport failures (`APITimeoutError` is a subclass carrying `.timeoutMs`), and `APIUserAbortError` for caller cancellation. By default the client retries 408/429/5xx plus connection errors and timeouts, twice, with 500 ms → 5 s exponential backoff and 25 % jitter, honouring `Retry-After` up to 60 s.

## Hierarchy

```
Error
└── TypeSafeError                     base class for SDK errors
    ├── APIError                      an unsuccessful HTTP response
    │   ├── BadRequestError           400
    │   ├── AuthenticationError       401
    │   ├── PermissionDeniedError     403
    │   ├── NotFoundError             404
    │   ├── UnprocessableEntityError  422
    │   ├── RateLimitError            429  (+ retryAfterMs)
    │   └── InternalServerError       5xx
    ├── APIConnectionError            DNS/TLS/connection closed, interrupted body
    │   └── APITimeoutError           timeout  (+ timeoutMs)
    └── APIUserAbortError             caller aborted via AbortSignal
```

All twelve classes are exported from the package root. `error.name` is set from `new.target.name` in the `TypeSafeError` constructor, so `err.name` is the concrete subclass name (`"RateLimitError"`, not `"Error"`).

## Class reference

### `TypeSafeError`

"Base class for SDK errors." Extends `Error`.

```ts
new TypeSafeError(message: string, options?: ErrorOptions): TypeSafeError
```

| Parameter | Type | Required | Description |
|---|---|---|---|
| `message` | `string` | yes | Error message. |
| `options` | `ErrorOptions` | no | Standard `{ cause }`. |

Properties: those of `Error` (`message`, `name`, `stack`, `cause`). `name` is set to the constructing subclass's name.

Thrown directly (not as a subclass) for client-side problems:

| Situation | Message |
|---|---|
| Missing API key | ``No API key was provided. Pass `apiKey` to the TypeSafeClient constructor or set the TYPESAFE_API_KEY environment variable.`` |
| No global fetch | ``No global `fetch` is available in this runtime. Pass a `fetch` implementation to the TypeSafeClient constructor.`` |
| Browser detected | ``TypeSafeClient is running in a browser, which would expose your API key to anyone using the page. Call the API from a server instead, or pass `dangerouslyAllowBrowser: true` if you understand the risk.`` |
| Empty question set | `At least one question is required.` |
| `score()` given an object | `Score criteria must be a list of descriptions indexed by score from zero, not a map.` |
| `choice()` given an array | `Choice criteria must be a map of labels to descriptions, not a list.` |
| Score question not a list | `Score question "<name>" has criteria that are not a list; score criteria must be a list of descriptions indexed by score from zero.` |
| Score question too short | `Score question "<name>" has N criteria; at least two scores are required.` |
| Invalid log level | `Invalid log level "X" from <source>. Expected one of: debug, info, warn, error, off.` |
| Bad `timeout` | ``` `timeout` must be a positive number of milliseconds, got X. ``` |
| Bad `retry.maxRetries` | ``` `retry.maxRetries` must be a non-negative integer, got X. ``` |
| Bad `retry.backoffInitialMs` / `backoffMaxMs` / `maxRetryAfterMs` | ``` `retry.<field>` must be a non-negative number of milliseconds, got X. ``` |
| Bad `retry.backoffJitter` | ``` `retry.backoffJitter` must be between 0 and 1, got X. ``` |
| Bad `retry.httpStatuses` entry | ``` `retry.httpStatuses` must contain HTTP status codes, got X. ``` |
| Unexpected `GET /v1/models` shape | `Unexpected response shape from GET /v1/models; expected { models: [...] }.` |

### `APIError`

"An unsuccessful HTTP response from the API." Extends `TypeSafeError`.

```ts
new APIError(status: number, body: unknown, headers: Headers, message?: string): APIError
```

| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| `status` | `number` | yes | — | HTTP status code. |
| `body` | `unknown` | yes | — | Parsed body. |
| `headers` | `Headers` | yes | — | Response headers. |
| `message` | `string` | no | derived (see below) | Overrides the derived message. |

Properties (all `readonly`, inherited unchanged by every subclass):

| Property | Type | Description |
|---|---|---|
| `status` | `number` | HTTP response status code. |
| `headers` | `Headers` | HTTP response headers. |
| `body` | `unknown` | Parsed JSON, response text, or `undefined` for an empty body. |
| `requestId` | `string \| undefined` | Request ID from `x-typesafe-request-id`, or `undefined` when absent. |

Static method:

```ts
static fromResponse(status: number, body: unknown, headers: Headers): APIError
```

"Create the error subclass for an HTTP status code." This is what the client calls for every non-2xx response.

**Derived message.** `APIError` builds `message` as `"<status> <detail>"`, where `detail` is pulled from the body in this order: a plain string body; `body.error` when it is a string; `body.error.message`; `body.message`; `body.detail` when it is a string; `body.detail.message`; or, when `body.detail` is an array, FastAPI-style validation entries formatted as semicolon-separated `path: message` pairs (with a leading `body` segment in `loc` dropped). If nothing matches: `"<status> status code (no body)"` for an empty body, otherwise the raw body (JSON-stringified if needed), truncated to 200 characters with an ellipsis.

### Status → class mapping

Exactly as implemented in `APIError.fromResponse`:

| HTTP status | Class | Doc description |
|---|---|---|
| 400 | `BadRequestError` | HTTP 400: the request is invalid. |
| 401 | `AuthenticationError` | HTTP 401: authentication failed. |
| 403 | `PermissionDeniedError` | HTTP 403: access is denied. |
| 404 | `NotFoundError` | HTTP 404: the resource was not found. |
| 422 | `UnprocessableEntityError` | HTTP 422: request validation failed. |
| 429 | `RateLimitError` | HTTP 429: the rate limit was exceeded. |
| ≥ 500 | `InternalServerError` | HTTP 5xx: the server failed to handle the request. |
| any other non-2xx (e.g. 402, 405, 408, 409, 418) | `APIError` | base class, no subclass |

Note the gaps: **408 has no dedicated class** even though it is retried by default, and 402/409 fall through to plain `APIError`. `BadRequestError`, `AuthenticationError`, `PermissionDeniedError`, `NotFoundError`, `UnprocessableEntityError` and `InternalServerError` add no properties or methods of their own — they exist purely so you can branch with `instanceof`.

### `RateLimitError`

Extends `APIError`; everything above plus:

| Property | Type | Description |
|---|---|---|
| `retryAfterMs` | `number \| undefined` | Server retry delay in milliseconds, or `undefined` when absent or invalid. |

Computed at construction from the response headers by `parseRetryAfter`, which prefers `retry-after-ms` (a finite non-negative number) and otherwise parses `Retry-After` either as seconds (converted to ms; negative → `undefined`) or as an HTTP date (converted to a non-negative delta from now).

### `APIConnectionError`

"The request or response-body delivery failed (DNS, TLS, connection closed, etc.)." Extends `TypeSafeError`.

```ts
new APIConnectionError(message?: string, options?: ErrorOptions): APIConnectionError
```

| Parameter | Type | Required | Default |
|---|---|---|---|
| `message` | `string` | no | `"Connection error."` |
| `options` | `ErrorOptions` | no | — |

No extra properties. In practice the client constructs it with `` `Connection error: ${err.message}` `` and `{ cause: err }`, so the original `TypeError`/`fetch` failure is on `.cause`.

### `APITimeoutError`

"The full response did not arrive within the timeout. A kind of `APIConnectionError`." Extends `APIConnectionError`.

```ts
new APITimeoutError(timeoutMs: number, options?: ErrorOptions): APITimeoutError
```

| Parameter / Property | Type | Required | Description |
|---|---|---|---|
| `timeoutMs` (param) | `number` | yes | The configured timeout. |
| `timeoutMs` (property, `readonly`) | `number` | — | Configured timeout in milliseconds. |
| `options` | `ErrorOptions` | no | — |

Message: `Request timed out after <timeoutMs>ms.` Because it extends `APIConnectionError`, `catch (e) { if (e instanceof APIConnectionError) … }` also catches timeouts — check `APITimeoutError` **first** if you need to tell them apart.

The timeout covers the whole round trip *including body delivery*: the client buffers the full response body under the same abort controller before resolving.

### `APIUserAbortError`

"The caller cancelled the request through an `AbortSignal`." Extends `TypeSafeError`.

```ts
new APIUserAbortError(message?: string, options?: ErrorOptions): APIUserAbortError
```

| Parameter | Type | Required | Default |
|---|---|---|---|
| `message` | `string` | no | `"Request was aborted."` |
| `options` | `ErrorOptions` | no | — |

Raised both when the signal fires during an attempt and when it fires while waiting to retry. It is **never retried**, regardless of policy.

## `RetryPolicy`

"Retry configuration. Partial overrides inherit unset fields from the client or SDK defaults." All fields are `readonly` and required on the full interface; you pass `Partial<RetryPolicy>` to the client or to a call.

| Property | Type | Required (in `Partial`) | Default | Description |
|---|---|---|---|---|
| `maxRetries` | `number` | no | `2` | Maximum retries after the initial attempt; `0` disables retries. Must be a non-negative integer. |
| `backoffInitialMs` | `number` | no | `500` | First backoff delay in milliseconds, doubled up to `backoffMaxMs`. Must be non-negative and finite. |
| `backoffMaxMs` | `number` | no | `5000` | Maximum backoff delay in milliseconds. Must be non-negative and finite. |
| `backoffJitter` | `number` | no | `0.25` | Fraction of each backoff delay randomly subtracted, from 0 to 1. |
| `httpStatuses` | `ReadonlySet<number>` | no | `new Set([408, 429, 500…599])` | HTTP status codes to retry. Entries must be integers in 100–999. |
| `respectRetryAfter` | `boolean` | no | `true` | Honor `Retry-After` and `retry-after-ms` up to `maxRetryAfterMs`. |
| `maxRetryAfterMs` | `number` | no | `60000` | Maximum server retry delay in milliseconds; longer delays use backoff. |
| `apiConnectionError` | `boolean` | no | `true` | Retry connection failures, including interrupted response bodies (`APIConnectionError`). |
| `apiTimeoutError` | `boolean` | no | `true` | Whether to retry `APITimeoutError`. |

The defaults come from `DEFAULT_RETRY_POLICY` in `src/retry.ts`; `DEFAULT_MAX_RETRIES` is exported internally as `2` but is not part of the public package surface. `DEFAULT_TIMEOUT_MS` is `10_000`.

### How a delay is computed

`retryDelayMs(attempt, headers, policy)` (zero-based `attempt`):

1. If `respectRetryAfter` and response headers are present, parse `retry-after-ms` / `Retry-After`; if the value is defined and `<= maxRetryAfterMs`, **use it as-is** (no jitter).
2. Otherwise: `exponential = min(backoffInitialMs * 2 ** attempt, backoffMaxMs)`, then `round(exponential * (1 - random() * backoffJitter))`.

With the defaults that is roughly 375–500 ms before the first retry and 750–1000 ms before the second.

### What is retried

| Failure | Retried by default? | Governed by |
|---|---|---|
| HTTP 408, 429, 500–599 | yes | `httpStatuses` |
| Other non-2xx (400, 401, 403, 404, 422, …) | no | `httpStatuses` |
| `APIConnectionError` | yes | `apiConnectionError` |
| `APITimeoutError` | yes | `apiTimeoutError` (checked before `apiConnectionError`, despite the subclass relationship) |
| `APIUserAbortError` | **never** | — |
| Client-side `TypeSafeError` (validation, config) | never (thrown before any HTTP) | — |

Retries are counted with an `X-TypeSafe-Retry-Count` header (`"1"`, `"2"`, …; absent on the first attempt). Each retry logs at `info`: `#N POST /v1/systemone retrying in 412ms (retry 1/2) after 429`.

Overrides are merged field by field: a call's `retry` inherits unset fields from the client's resolved policy, which itself inherits from `DEFAULT_RETRY_POLICY`. The status set is copied on merge, so mutating a `Set` you passed in afterwards has no effect.

## `RequestOptions`

"Per-call options that override client settings." All optional; accepted as the second argument of `client.systemOne()` and the first of `client.models.list()`.

| Property | Type | Required | Default | Description |
|---|---|---|---|---|
| `signal` | `AbortSignal` | no | — | Cancellation signal for the request **and pending retries**. |
| `timeout` | `number` | no | client `timeout` (`10000`) | Timeout per attempt in milliseconds; there is no total retry budget. Must be a positive finite number. |
| `retry` | `Partial<RetryPolicy>` | no | client `retry` | Retry overrides for this call; omitted fields inherit client settings. |
| `headers` | `Record<string, string>` | no | — | Additional headers, merged over `defaultHeaders`. |

Header merging is case-insensitive and last-value-wins; the SDK's own auth and protocol headers are applied *after* yours, so they cannot be overridden (see [[reference/javascript-sdk]]).

There is no `maxRetries` shorthand — write `{ retry: { maxRetries: 0 } }`.

## Example: try/catch with the full hierarchy

```ts
import {
  APIConnectionError,
  APIError,
  APITimeoutError,
  APIUserAbortError,
  AuthenticationError,
  BadRequestError,
  InternalServerError,
  NotFoundError,
  PermissionDeniedError,
  RateLimitError,
  TypeSafeClient,
  TypeSafeError,
  UnprocessableEntityError,
  choice,
} from "@typesafe-ai/sdk";

const client = new TypeSafeClient({
  timeout: 15_000,
  retry: { maxRetries: 3, backoffInitialMs: 250, maxRetryAfterMs: 30_000 },
});

const controller = new AbortController();
setTimeout(() => controller.abort(), 30_000); // hard ceiling across all attempts

try {
  const { answers } = await client.systemOne(
    {
      state: { ticket: "I was charged twice." },
      questions: {
        category: choice("What is this ticket about?", {
          billing: null,
          technical: null,
          other: null,
        }),
      },
    },
    {
      signal: controller.signal,
      timeout: 8_000,
      retry: { httpStatuses: new Set([429, 500, 502, 503, 504]) },
      headers: { "X-Correlation-Id": "abc-123" },
    },
  );
  console.log(answers.category.choice);
} catch (err) {
  // Order matters: subclasses before their bases.
  if (err instanceof RateLimitError) {
    console.error(`rate limited; retry after ${err.retryAfterMs ?? "unknown"}ms`, err.requestId);
  } else if (err instanceof AuthenticationError || err instanceof PermissionDeniedError) {
    console.error(`auth problem ${err.status}:`, err.body); // do not retry
  } else if (err instanceof BadRequestError || err instanceof UnprocessableEntityError) {
    console.error(`bad request ${err.status}:`, err.message); // fix the payload
  } else if (err instanceof NotFoundError) {
    console.error("wrong baseURL or model?", err.status);
  } else if (err instanceof InternalServerError) {
    console.error(`server error ${err.status} (request ${err.requestId ?? "unknown"})`);
  } else if (err instanceof APIError) {
    // 402, 405, 409, 418, ... land here
    console.error(`API error ${err.status}:`, err.body, err.headers.get("x-typesafe-request-id"));
  } else if (err instanceof APITimeoutError) {
    console.error(`timed out after ${err.timeoutMs}ms`, err.cause);
  } else if (err instanceof APIConnectionError) {
    console.error("transport failure:", err.message, err.cause);
  } else if (err instanceof APIUserAbortError) {
    console.error("cancelled by caller");
  } else if (err instanceof TypeSafeError) {
    console.error("client-side problem:", err.message); // validation or config
  } else {
    throw err;
  }
}
```

CommonJS is identical apart from the import:

```js
const { APIError, RateLimitError, TypeSafeClient, choice } = require("@typesafe-ai/sdk");
```

The minimal upstream form, from `examples/demo.ts`:

```ts
try {
  // ... client.systemOne(...)
} catch (err) {
  if (err instanceof APIError) {
    console.error(`API error ${err.status} (request ${err.requestId ?? "unknown"}):`, err.body);
  } else {
    throw err;
  }
}
```

## Gotchas

- **`APITimeoutError extends APIConnectionError`**, so a bare `instanceof APIConnectionError` branch swallows timeouts. Check the more specific class first.
- **No total retry budget.** `timeout` is per attempt; with `maxRetries: 3` a hung endpoint can consume 4 × `timeout` plus backoff. Use an `AbortSignal` for a wall-clock ceiling — it cancels pending retries too.
- **`Retry-After` is used verbatim** when `<= maxRetryAfterMs`, with no jitter, so a fleet retrying together will retry together. Lower `maxRetryAfterMs` or set `respectRetryAfter: false` if that matters.
- **408 is retried but has no subclass** — it surfaces as a plain `APIError` with `status === 408`.
- **`body` is `unknown`.** Narrow it before use; it may be a parsed object, a raw string, or `undefined`.
- **Aborting mid-body-read** raises `APIUserAbortError`, not `APIConnectionError`, because the body buffer shares the caller's signal.
- **`err.requestId` is the thing to log.** It comes from `x-typesafe-request-id` and is what TypeSafe support will ask for.

## Version notes

Described for `@typesafe-ai/sdk` 0.6.0 (repo commit `66880ccded6cb642dc1809620c2b108c33730214`, 2026-09-15). Docs and source agree on every class, property and default listed here; the published per-class pages simply omit the message-derivation logic and the `TypeSafeError` message catalogue, which come from `src/errors.ts` and `src/client.ts`.

## Related

- [[reference/javascript-sdk]] — client construction, defaults, headers
- [[reference/javascript-sdk-types]] — `RetryPolicy` and `RequestOptions` in the wider type map
- [[reference/rate-limits-and-errors]] — HTTP status semantics across all clients
- [[reference/python-sdk-retries-errors]] — the Python equivalents
- [[reference/http-api]] — status codes as the API defines them

## Sources

- raw/docs/sdk__javascript__api__classes__TypeSafeError.md, __APIError.md, __APIConnectionError.md, __APITimeoutError.md, __APIUserAbortError.md, __BadRequestError.md, __AuthenticationError.md, __PermissionDeniedError.md, __NotFoundError.md, __UnprocessableEntityError.md, __RateLimitError.md, __InternalServerError.md (https://docs.typesafe.ai/sdk/javascript/api/classes/*)
- raw/docs/sdk__javascript__api__interfaces__RetryPolicy.md (https://docs.typesafe.ai/sdk/javascript/api/interfaces/RetryPolicy)
- raw/docs/sdk__javascript__api__interfaces__RequestOptions.md (https://docs.typesafe.ai/sdk/javascript/api/interfaces/RequestOptions)
- raw/github/typesafe-sdk-js/src/errors.ts, src/retry.ts, src/client.ts, src/types.ts, examples/demo.ts (commit 66880ccded6cb642dc1809620c2b108c33730214)
