Flat Dispatch
Flat dispatch is the third argument to expectStatus. It’s a single object where the value type determines the behavior:
| Value type | Meaning |
|---|---|
| Function | Handler — receives typed body, can return or throw |
| String | Message — throws an ExpectStatusError with this text |
No { handlers: {}, messages: {} } nesting required.
Handlers
Section titled “Handlers”A function entry receives the typed body for that status branch. It can return a value (which becomes the overall result) or throw.
const result = await expectStatus(201, api.createOrg({ body: data }), { 409: ({ orgId }) => redirect(`/org/${orgId}`), // return → becomes the result 422: (body) => { throw new Error(body.errors.join(", ")); // throw → propagates },});Handler bodies are automatically typed from the response union — no casts needed.
Return type widening
Section titled “Return type widening”When a handler returns a value, the overall return type widens to include the handler’s return type:
// Without handlers: Promise<Membership>const a = await expectStatus(200, response);
// With returning handler: Promise<Membership | { conflict: string }>const b = await expectStatus(200, response, { 409: (body) => ({ conflict: body.orgId }),});When transform or recover is provided, the return type widens to Promise<unknown>.
Messages
Section titled “Messages”A string entry auto-throws an ExpectStatusError with that string as the message:
await expectStatus(201, api.createOrg({ body: data }), { 409: "An organisation with that name already exists.", 422: "Please check your input.",});Valid dispatch keys
Section titled “Valid dispatch keys”Dispatch keys can be:
- Exact status codes —
409,422,500 - Hundred-level ranges —
'4xx','5xx','3xx','2xx','1xx' - Custom group names —
'auth','retryable'(instance-defined)
Note: Built-in groups
'success'and'error'are not valid dispatch keys. Use ranges ('4xx','5xx') or define custom groups instead.
Specificity
Section titled “Specificity”Within a single source (per-call or defaults), the most specific key wins:
- Exact code —
409beats'4xx' - Range —
'4xx'beats a custom group - Custom group —
'auth'is the broadest
await expectStatus(200, response, { 404: "Not found.", // exact code — highest priority "4xx": "Client error.", // range — catches 400, 401, 403, etc. "5xx": "Service is temporarily unavailable.",});Handlers vs messages priority
Section titled “Handlers vs messages priority”Handlers (functions) always take priority over messages (strings), even across per-call and instance defaults. The full lookup order is:
- Per-call handler (most specific key)
- Instance default handler (most specific key)
- Per-call message (most specific key)
- Instance default message (most specific key)
This means an instance handler will beat a per-call message at the same status. If you need per-call to always win, use the same value type (both functions, or both strings).
Per-call vs defaults
Section titled “Per-call vs defaults”Within the same value type, per-call always shadows instance defaults:
const expectStatus = createExpectStatus({ defaults: { 404: "Not found (default)", "5xx": "Server error (default)", },});
await expectStatus(200, response, { 404: "Custom not found", // shadows the default for 404 // '5xx' still uses the default});Mixing handlers and messages
Section titled “Mixing handlers and messages”Mix freely in the same object:
await expectStatus(200, api.acceptInvite({ body }), { 409: ({ orgId }) => redirect(`/org/${orgId}`), // handler 422: "Please check your input.", // message "5xx": "Service is temporarily unavailable.", // range message});Reserved keys
Section titled “Reserved keys”Some keys have special meaning and are not treated as status dispatch:
exhaustive— enable compile-time exhaustiveness checkingtransform— reshape the success bodyrecover— catch-all error handlerthrows— set tofalsefor SafeResult modeonError/onSuccess— per-call observability hooks
Next steps
Section titled “Next steps”- Status Specifiers — all the forms the first argument accepts
- Error Resolution — the full priority order when a status doesn’t match
- Recover & Transform — return-value hooks