Skip to main content

Errors & retries

The SDK throws exactly two error classes — there are no per-status subclasses and no result objects.

import { OptoLinkError, OptoLinkConnectionError } from "@optomatica/optolink-sdk";

The error classes​

OptoLinkError — any non-2xx response:

PropertyTypeMeaning
statusnumberHTTP status code
messagestringThe backend error message
requestIdstring | undefinedPresent when the error envelope carries one
details{ message: string; field?: string }[] | undefinedPresent on 400 validation failures only — message text (no field names today)
retryAfterSecondsnumber | undefinedParsed from 429 bodies (see below)
rawunknownThe full error body, verbatim

OptoLinkConnectionError extends OptoLinkError — thrown when the request never got an HTTP response: network failure, connection reset, or timeout. status is 0.

try {
const link = await client.links.get(id);
} catch (err) {
if (err instanceof OptoLinkConnectionError) {
// network / timeout — status is 0
} else if (err instanceof OptoLinkError) {
console.error(err.status, err.message, err.details);
}
}

Non-JSON error bodies (a proxy's HTML 502, an empty body) still throw OptoLinkError; message falls back to OptoLink request failed with status <status> and raw holds the body text.

The error envelope​

Backend failures return a JSON envelope; the SDK lifts its fields onto the error and keeps the whole thing on err.raw:

{
"statusCode": 404,
"error": "Not Found",
"message": "Link <id> not found",
"details": [{ "message": "…" }],
"timestamp": "2026-09-20T05:30:58.091Z",
"path": "/links/<id>",
"requestId": "…"
}

details[] appears only on 400 validation errors (a non-UUID id, for example, is a 400 — Validation failed (uuid is expected) — with no details[]). requestId appears only when the request carried an x-request-id header; the SDK does not send one by default.

Common statuses​

StatusmessageWhen
400Validation failed + details[]Body/query validation errors
400Validation failed (uuid is expected)Non-UUID id argument
401Invalid API keyUnknown, revoked, or expired key
401Missing Authorization header / Invalid Authorization format / Invalid API key formatMalformed auth
401Organization is suspendedSuspended organization
403API key tier 'CLIENT' is not permitted on this endpoint (requires 'SERVER')CLIENT-tier key on the links API
404Link <id> not foundMissing link, or another org's link
429Rate limit exceeded. Retry after N second(s).Rate limited (see below)
500variesBackend fault — e.g. a duplicate shortCode surfaces a raw database error. Surfaced as-is, never papered over

Rate limits (429)​

The API sends no rate-limit headers — retry timing is communicated in the body only, as Rate limit exceeded. Retry after N second(s). The SDK parses N into err.retryAfterSeconds and, within the retry budget, sleeps that long before retrying automatically (a 429 whose body doesn't parse falls back to the standard backoff below).

The retry matrix​

Defaults: timeout: 10_000 ms per attempt, retries: 2 (one shared budget per public call — see install).

FailureWrites (create, update, delete)Reads (get, list, listAll, qrSvg, qrPng)
Connection error / timeoutretryretry
429retry, sleeping retryAfterSecondsretry, same
5xxnever — a retried POST that already landed would create a second linkretry
Other 4xxnevernever

Writes are never retried on 5xx because the backend has no idempotency keys. Reads are always safe to replay.

Backoff between attempts: 429 sleeps retryAfterSeconds; every other retryable failure sleeps min(0.5 · 2ⁿ, 5) seconds (n = attempt index).

One shared budget: create and update chain a write plus the hidden refetch GET — the pair draws from a single retry counter, so the refetch never gets a fresh budget. When the budget is exhausted, the last error is thrown.