API Integration and Error Handling
Use api integration and error handling with the current published Askr packages.
@askrjs/askr0.0.59
How to use api integration and error handling
Define the endpoint once, pass cancellation through every request, and branch on the typed result before reading data.
- Import the published @askrjs/askr entrypoint.
- Keep api integration and error handling configuration next to the component, route, or server composition root that owns it.
- Handle pending, unavailable, cancellation, and failure states, then verify the production path.
import { createClient, defineApi, get, json } from '@askrjs/fetch';
const api = createClient(defineApi({
project: get('/projects/{id}').params<{ id: string }>().returns(json<Project>()),
}), { baseUrl: env.API_URL });
const result = await api.project({ params: { id }, signal });
if (!result.ok) return <ProjectError problem={result.error} />;
return <ProjectDetails project={result.data} />;Goal and architecture
`@askrjs/fetch` gives you a typed client built from `defineApi({...})` descriptors, where each endpoint declares its path, params, query, and response codecs with `get`/`post`/`put`/`patch`/`del`. Every call resolves to a discriminated `ClientResult` instead of throwing, so error handling is a type-narrowing problem, not a try/catch problem. That result carries an `ok` boolean plus a `kind` you can switch on — `'http'` for a non-2xx response your API described, or transport-level kinds for network, timeout, and decode failures — which keeps error branches as visible in the type system as the success branch.
Implementation
Call `createClient(api, { baseUrl, middleware })` once per backend and reuse it; add `bearerAuth`, `logging`, `retry`, or `telemetry` from `@askrjs/fetch/middleware` in whatever order you want them to run, since middleware executes outward in declaration order. Declare response and error shapes with `.returns(200, json<T>())` and `.errors({ 404: json<E>() })` so a 404 decodes into your typed error body instead of falling through to a generic failure. For requests without a full API descriptor, `createFetch({ baseUrl })` gives you the same result/codec/middleware machinery for one-off calls, like a health check or a webhook receiver.
Failure states
Branch on `result.ok` first, then on `result.kind` for the failure path: `'http'` means the server responded with a status your API declared (read `result.status` and `result.error`), while other kinds cover network drops, `AbortSignal` timeouts (`kind: 'timeout'`), caller cancellation (`kind: 'abort'`), and decode failures when a response doesn't match its codec. Don't collapse all of these into one generic 'something went wrong' toast — a timeout usually means 'retry', an HTTP 404 usually means 'not found, don't retry', and a decode failure means your client and server have drifted. The built-in `retry` middleware only retries idempotent methods (GET, HEAD, PUT, DELETE, OPTIONS) on specific statuses (408, 425, 429, 500, 502, 503, 504) and honors `Retry-After`, so requests with bodies won't get silently duplicated.
Verification
Assert against the shape of `ClientResult` in tests — construct expected success and failure branches and confirm your UI renders the right thing for each `kind`, not just the happy path. Use `unwrap(result)` in test helpers where throwing is more convenient than narrowing, but keep production code on the discriminated result. For codec correctness, feed a validator (`safeParse`) a value that fails and confirm the client surfaces a `decode` failure rather than crashing, since a validator's `safeParse` return type dictates exactly what shape errors take upstream.