JavaScript SDK error classes, RetryPolicy, RequestOptions
TL;DR Every throw from
@typesafe-ai/sdk0.6.0 is aTypeSafeError. CatchAPIErrorfor HTTP failures (.status,.headers,.body,.requestId;RateLimitErroradds.retryAfterMs),APIConnectionErrorfor transport failures (APITimeoutErroris a subclass carrying.timeoutMs), andAPIUserAbortErrorfor 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, honouringRetry-Afterup 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.
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.
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:
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.
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.
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.
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):
- If
respectRetryAfterand response headers are present, parseretry-after-ms/Retry-After; if the value is defined and<= maxRetryAfterMs, use it as-is (no jitter). - Otherwise:
exponential = min(backoffInitialMs * 2 ** attempt, backoffMaxMs), thenround(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 JavaScript/TypeScript SDK: install, client, choice/score/noul).
There is no maxRetries shorthand — write { retry: { maxRetries: 0 } }.
Example: try/catch with the full hierarchy
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:
const { APIError, RateLimitError, TypeSafeClient, choice } = require("@typesafe-ai/sdk");
The minimal upstream form, from examples/demo.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 bareinstanceof APIConnectionErrorbranch swallows timeouts. Check the more specific class first.- No total retry budget.
timeoutis per attempt; withmaxRetries: 3a hung endpoint can consume 4 ×timeoutplus backoff. Use anAbortSignalfor a wall-clock ceiling — it cancels pending retries too. Retry-Afteris used verbatim when<= maxRetryAfterMs, with no jitter, so a fleet retrying together will retry together. LowermaxRetryAfterMsor setrespectRetryAfter: falseif that matters.- 408 is retried but has no subclass — it surfaces as a plain
APIErrorwithstatus === 408. bodyisunknown. Narrow it before use; it may be a parsed object, a raw string, orundefined.- Aborting mid-body-read raises
APIUserAbortError, notAPIConnectionError, because the body buffer shares the caller's signal. err.requestIdis the thing to log. It comes fromx-typesafe-request-idand 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
- JavaScript/TypeScript SDK: install, client, choice/score/noul — client construction, defaults, headers
- JavaScript SDK interfaces and type aliases —
RetryPolicyandRequestOptionsin the wider type map - HTTP status codes, rate limits, retry semantics — HTTP status semantics across all clients
- Python SDK retries, exceptions, constants — the Python equivalents
- HTTP API: POST /v1/systemone and GET /v1/models — 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)