Skip to content

Flat Dispatch

Flat dispatch is the third argument to expectStatus. It’s a single object where the value type determines the behavior:

Value typeMeaning
FunctionHandler — receives typed body, can return or throw
StringMessage — throws an ExpectStatusError with this text

No { handlers: {}, messages: {} } nesting required.

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.

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>.

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.",
});

Dispatch keys can be:

  • Exact status codes409, 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.

Within a single source (per-call or defaults), the most specific key wins:

  1. Exact code409 beats '4xx'
  2. Range'4xx' beats a custom group
  3. 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 (functions) always take priority over messages (strings), even across per-call and instance defaults. The full lookup order is:

  1. Per-call handler (most specific key)
  2. Instance default handler (most specific key)
  3. Per-call message (most specific key)
  4. 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).

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
});

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
});

Some keys have special meaning and are not treated as status dispatch:

  • exhaustive — enable compile-time exhaustiveness checking
  • transform — reshape the success body
  • recover — catch-all error handler
  • throws — set to false for SafeResult mode
  • onError / onSuccess — per-call observability hooks