Examples
Every example assumes a status-discriminated response union like this:
type CreateOrgResponse = | { status: 201; body: { id: string; name: string } } | { status: 409; body: { message: string; orgId: string } } | { status: 422; body: { message: string; field: string } } | { status: 500; body: { message: string } }Basic usage
Section titled “Basic usage”import { expectStatus } from 'expect-status'
const org = await expectStatus(201, client.createOrg({ body: data }))// ^? { id: string; name: string }Non-201 statuses throw an ExpectStatusError with a message extracted from the response body.
Multiple success statuses
Section titled “Multiple success statuses”const result = await expectStatus([200, 201], client.upsert({ body: data }))// ^? body of 200 | body of 201Named specifiers
Section titled “Named specifiers”// Built-in groupsawait expectStatus('success', client.health()) // 200-299await expectStatus('error', response) // 400-599
// Rangesawait expectStatus('2xx', response) // 200-299await expectStatus('4xx', response) // 400-499await expectStatus('5xx', response) // 500-599
// Negationawait expectStatus('!4xx', response) // anything except 400-499await expectStatus('!error', response) // anything except 400-599
// Mixed arraysawait expectStatus([200, '3xx'], response) // 200 or any 300-399await expectStatus(['success', 404], response) // any 2xx or 404Flat dispatch — messages
Section titled “Flat dispatch — messages”Strings auto-throw an ExpectStatusError with that message:
await expectStatus(201, client.createOrg({ body: data }), { 409: 'An organisation with that name already exists.', 422: 'Please check your input.',})Flat dispatch — handlers
Section titled “Flat dispatch — handlers”Functions receive the typed body and can return or throw:
const org = await expectStatus(201, client.createOrg({ body: data }), { 409: ({ orgId }) => redirect(`/org/${orgId}`), // returns → becomes the result 422: (body) => { throw new Error(body.message) }, // throws → propagates})Flat dispatch — ranges
Section titled “Flat dispatch — ranges”Catch entire HTTP classes. Exact codes always take priority:
await expectStatus(200, response, { 404: 'Not found.', // exact code — highest priority '4xx': 'Client error.', // range — catches 400, 401, 403, ... '5xx': 'Service is temporarily unavailable.',})Returning handlers
Section titled “Returning handlers”Handlers can return values, widening the return type:
const result = await expectStatus(201, client.createOrg({ body: data }), { 409: (body) => ({ conflict: true, orgId: body.orgId }),})// result: { id: string; name: string } | { conflict: true; orgId: string }Custom groups
Section titled “Custom groups”Define domain-specific status groups on the instance:
const expectStatus = createExpectStatus({ groups: { auth: [401, 403], retryable: [408, 429, 500, 502, 503, 504], },})
// As expected statusawait expectStatus('auth', response)
// In dispatchawait expectStatus(200, response, { auth: () => redirect('/sign-in'), retryable: 'Please try again shortly.',})
// Negatedawait expectStatus('!auth', response)Recover
Section titled “Recover”recover wraps the entire error path. If it returns a non-undefined value, that becomes the result:
const config = await expectStatus(200, client.getFeatureFlags(), { recover: () => DEFAULT_FLAGS,})// config: FeatureFlags — never throwsTransform
Section titled “Transform”Reshape the success body before returning:
const wrapped = await expectStatus(200, response, { transform: (body) => ({ data: body, fetchedAt: Date.now() }),})// wrapped: { data: User; fetchedAt: number }SafeResult (throws: false)
Section titled “SafeResult (throws: false)”Get a discriminated union instead of throwing:
const result = await expectStatus(200, client.getUser(id), { throws: false,})
if (result.ok) { result.data // typed body} else { result.error // Error result.status // number result.body // unknown}Exhaustive checking
Section titled “Exhaustive checking”TypeScript flags uncovered error statuses at compile time:
await expectStatus(201, client.createOrg({ body: data }), { 409: 'Conflict', 422: 'Invalid input', 500: 'Server error', exhaustive: true, // ✅ compiles — all error statuses covered})
await expectStatus(201, client.createOrg({ body: data }), { 409: 'Conflict', exhaustive: true, // ❌ type error — missing 422, 500})Observability hooks
Section titled “Observability hooks”Fire side-effects on every error or success without affecting the result:
const expectStatus = createExpectStatus({ onError: (err, response) => { Sentry.captureException(err, { extra: { status: response.status, body: response.body }, }) }, onSuccess: (response) => { analytics.track('api_success', { status: response.status }) },})Per-call hooks shadow instance hooks:
await expectStatus(200, response, { onError: (err) => customLogger.warn(err.message),})Instance defaults
Section titled “Instance defaults”Set shared handlers and messages for your entire app:
import { createExpectStatus } from 'expect-status'
export const expectStatus = createExpectStatus({ fallbackMessage: 'Something went wrong. Please try again.', groups: { auth: [401, 403] }, defaults: { auth: 'Please sign in or check your permissions.', 429: 'Too many requests. Please slow down.', '5xx': 'Service is temporarily unavailable.', }, onError: (err, res) => { Sentry.captureException(err, { extra: { status: res.status } }) },})
// Per-call dispatch shadows defaultsawait expectStatus(200, response, { '5xx': (body) => customFallback(body), // shadows instance '5xx'})Custom error class
Section titled “Custom error class”Replace ExpectStatusError with your own:
class ApiError extends Error { constructor(message: string, public status: number, public body: unknown) { super(message) }}
const expectStatus = createExpectStatus({ errorFactory: (message, response) => new ApiError(message, response.status, response.body),})Message extraction
Section titled “Message extraction”Customise how messages are pulled from response bodies:
import { createExpectStatus, chainExtractors, problemDetail, messageField,} from 'expect-status'
const expectStatus = createExpectStatus({ extractMessage: chainExtractors(problemDetail, messageField),})Write your own extractor:
const reasonField = (body: unknown) => typeof body === 'object' && body !== null && 'reason' in body ? String((body as { reason: string }).reason) : undefined
const expectStatus = createExpectStatus({ extractMessage: chainExtractors(reasonField, problemDetail, messageField),})Adapter (Axios)
Section titled “Adapter (Axios)”Normalize non-standard response shapes at the instance level:
import axios from 'axios'import { createExpectStatus } from 'expect-status'
const api = axios.create({ baseURL: 'https://api.example.com', validateStatus: () => true,})
const expectStatus = createExpectStatus({ adapter: (res) => ({ status: res.status, body: res.data }),})
const org = await expectStatus(201, api.post('/orgs', data))Adapter (custom envelope)
Section titled “Adapter (custom envelope)”const expectStatus = createExpectStatus({ adapter: (res) => ({ status: res.meta.httpStatus, body: res.result.data, }),})Adapter (async — native fetch)
Section titled “Adapter (async — native fetch)”const expectStatus = createExpectStatus({ adapter: async (res) => ({ status: res.status, body: await res.json(), }),})
const user = await expectStatus(200, fetch('/api/user'))fetchExpect (native fetch helper)
Section titled “fetchExpect (native fetch helper)”import { fetchExpect } from 'expect-status/fetch'
type UserResponse = | { status: 200; body: { id: string; name: string } } | { status: 404; body: { message: string } }
const user = await fetchExpect<UserResponse>( 'https://api.example.com/users/1', 200, { 404: 'User not found', init: { headers: { Authorization: 'Bearer token' } }, },)Custom field names
Section titled “Custom field names”For APIs that use different field names:
const expectStatus = createExpectStatus({ statusField: 'code', bodyField: 'payload',})
type Response = | { code: 200; payload: { id: string } } | { code: 404; payload: { message: string } }
const item = await expectStatus(200, response)Real-world patterns
Section titled “Real-world patterns”TanStack Query
Section titled “TanStack Query”function useOrganisation(id: string) { return useQuery({ queryKey: ['org', id], queryFn: () => expectStatus(200, client.getOrg({ params: { id } })), })}
function useCreateOrganisation() { return useMutation({ mutationFn: (data: CreateOrgInput) => expectStatus(201, client.createOrg({ body: data }), { 409: (body) => redirect(`/org/${body.orgId}`), 422: 'Invalid organisation details.', }), })}Form submissions
Section titled “Form submissions”async function onSubmit(formData: FormData) { try { const org = await expectStatus(201, client.createOrg({ body: formData }), { 409: 'An organisation with that name already exists.', 422: 'Please check the form and try again.', }) redirect(`/org/${org.id}`) } catch (err) { toast.error(err.message) }}Form submissions with recover
Section titled “Form submissions with recover”async function onSubmit(formData: FormData) { const result = await expectStatus(201, client.createOrg({ body: formData }), { 409: 'An organisation with that name already exists.', recover: (err) => ({ error: err.message }), })
if ('error' in result) { toast.error(result.error) } else { redirect(`/org/${result.id}`) }}Next.js Server Action
Section titled “Next.js Server Action”'use server'
import { expectStatus } from '@/lib/expect-status'import { redirect } from 'next/navigation'
export async function createOrganisation(formData: FormData) { const org = await expectStatus(201, client.createOrg({ body: { name: formData.get('name') }, }), { 409: 'An organisation with that name already exists.', 422: 'Please check the form and try again.', }) redirect(`/org/${org.id}`)}Error boundaries (React)
Section titled “Error boundaries (React)”async function loadOrganisation(id: string) { return expectStatus(200, client.getOrg({ params: { id } }), { 404: 'Organisation not found.', })}// Non-success statuses throw → caught by the nearest ErrorBoundaryTanStack Query with SafeResult
Section titled “TanStack Query with SafeResult”function useCreateOrganisation() { return useMutation({ mutationFn: async (data: CreateOrgInput) => { const result = await expectStatus(201, client.createOrg({ body: data }), { throws: false, }) if (!result.ok) { return { error: result.error.message } } return { data: result.data } }, })}Multiple instances
Section titled “Multiple instances”// Strict instance for user-facing callsexport const expectStatus = createExpectStatus({ fallbackMessage: 'Something went wrong.', onError: (err) => Sentry.captureException(err),})
// Lenient instance for background jobsexport const expectBg = createExpectStatus({ fallbackMessage: 'Background task failed.', onError: (err) => logger.warn(err.message),})Integration guides
Section titled “Integration guides”For full framework-specific setup, see: