Askr
Guides

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.

  1. Import the published @askrjs/askr entrypoint.
  2. Keep api integration and error handling configuration next to the component, route, or server composition root that owns it.
  3. 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.