Internally ctx.request resolves to an ApiResponse<T>:
interface ApiResponse<T> {
data: T // parsed body (null for 204/empty)
status: number // HTTP status
statusText?: string
headers: Record<string, string> // response headers
fromCache?: boolean // true when served from cache
}Return .data for a clean API, or return the whole envelope if callers need
status/headers:
getRaw: async (ctx, id: string) =>
ctx.request<Invoice>({ method: 'GET', path: '/invoices/{id}', pathParams: { id } }),
// caller: const { data, status, fromCache } = await api.invoices.getRaw('1')Every failure is one of these (all extend ApiError):
| Class | When |
|---|---|
ApiError |
Base; also used for generic 4xx/5xx. Has status, code, serverError, rawBody, retryCount, responseHeaders. |
NetworkError |
No response (offline, DNS, CORS). Flags: corsBlocked, offline, partial. |
TimeoutError |
Exceeded the configured timeout. Has timeoutMs. |
AuthError |
401 with no/failed refresh, or token getter failure. |
SchemaError |
Response validation failed (strict) or drift detected. |
SchemaParseError |
Malformed/unsupported OpenAPI spec (codegen/parse). |
ConfigurationError |
Bad config, missing path param, failing tenant resolver. |
import { ApiError, AuthError, TimeoutError } from '@developerehsan/api-client'
try {
await api.users.get('42')
} catch (err) {
if (err instanceof AuthError) redirectToLogin()
else if (err instanceof TimeoutError) toast('Timed out, retry?')
else if (err instanceof ApiError) console.error(err.status, err.serverError)
}See it live: the Feature Lab "Typed error (404)" button catches an ApiError
and reads status/code; the "Timeout" and "Cancellation" buttons catch
TimeoutError and AbortError respectively —
FeatureLab.tsx. The
direct-client demo surfaces ApiError.status + message in its UI —
DirectClientDemo.tsx.
{ code, message, details }bodies are parsed intoerror.serverError.- Non-JSON bodies (e.g. an HTML 5xx page) are kept in
error.rawBody; theerror.messageis a short, truncated hint (never the whole document).
createClient({ hooks: { onError: (error) => reportToSentry(error) } })Or subscribe imperatively: api.on('error', (err) => {}). See
hooks & events.
Return a discriminated union instead of throwing:
createClient({ safeMode: true })
const result = await api.users.get('42')
if (result.success) use(result.data)
else handle(result.error) // result.error is an ApiErrorAbortErrors still throw even in safeMode, so cancellation stays observable.
See it live: the Feature Lab creates a second client with safeMode: true
and shows the { success, error } result — see the safeApi client in
FeatureLab.tsx.