Skip to content

Exhaustiveness Checking

expect-status supports compile-time exhaustiveness checking to ensure all error statuses in your response union are covered by handlers or messages.

Set exhaustive: true to enable exhaustiveness checking:

type Response =
| { status: 200; body: { id: string } }
| { status: 404; body: { message: string } }
| { status: 409; body: { message: string; id: string } };
await expectStatus(200, response, {
404: (body) => {
/* handle 404 */
},
409: (body) => {
/* handle 409 */
},
exhaustive: true,
});

If you miss a status, TypeScript will error:

await expectStatus(200, response, {
404: (body) => {
/* handle 404 */
},
// Missing 409 - type error!
exhaustive: true,
});

String entries also count toward exhaustiveness — coverage means an exact entry or a matching range:

await expectStatus(200, response, {
404: (body) => {
/* handle 404 */
},
409: "Conflict",
exhaustive: true,
});

Range matchers like '4xx' count as covering all statuses in that class:

await expectStatus(200, response, {
"4xx": (body) => {
/* handles 404, 409, and all other 4xx */
},
exhaustive: true,
});

Instance-wide defaults count toward coverage. exhaustive itself is per-call only.

const expectStatus = createExpectStatus({
defaults: {
"5xx": "Server error",
},
});
await expectStatus(200, response, {
404: (body) => {
/* ... */
},
409: (body) => {
/* ... */
},
exhaustive: true,
});

Exhaustiveness checking is useful when:

  • You want to ensure all error cases are handled
  • You’re building a library or framework where completeness matters
  • You want compile-time safety for error handling

Skip exhaustiveness when:

  • You’re prototyping and don’t want to handle every error yet
  • You have a global error handler that catches everything
  • Your API has many error statuses and you only care about specific ones

A runtime guard also fires if exhaustive: true is bypassed at the type level (e.g. via as any) — surfacing the gap loudly rather than silently degrading to extractMessage / fallbackMessage.