Error Resolution
When the response status doesn’t match your expected status, expect-status resolves an error through a predictable priority chain.
Full resolution order
Section titled “Full resolution order”1. Status matches expected → onSuccess → transform → return2. Per-call handler (exact code → range → group)3. Instance default handler (exact code → range → group)4. Per-call message (exact code → range → group)5. Instance default message (exact code → range → group)6. extractMessage(body)7. fallbackMessage8. onError fires9. recover → return or re-throwHandlers (functions) are always checked before messages (strings), even across per-call and instance defaults. Within each tier, per-call shadows instance, and exact codes shadow ranges, which shadow groups.
Hooks summary
Section titled “Hooks summary”| Hook | Purpose | Fires when |
|---|---|---|
onSuccess | Observe success (analytics, logging) | Every success, before transform |
onError | Observe errors (logging, metrics) | Every error, before recover |
transform | Reshape success body | Success path, after onSuccess |
recover | Catch errors, return instead of throw | Error path (true catch-all) |
Success path
Section titled “Success path”When the status matches the expected status, expect-status:
- Calls
onSuccess(response)— per-call hook shadows instance hook - Calls
transform(body)if provided — reshapes the body before returning - Returns the (optionally transformed) body
await expectStatus(200, response, { onSuccess: (res) => analytics.track(res.status), transform: (body) => ({ data: body, ts: Date.now() }),});Error path
Section titled “Error path”Handlers (functions)
Section titled “Handlers (functions)”If the best-matching entry is a function, it receives the typed body. If it returns a value, that value becomes the overall result. If it throws, the error propagates (and may be caught by recover).
await expectStatus(200, response, { 409: ({ orgId }) => redirect(`/org/${orgId}`), // returns → result});Messages (strings)
Section titled “Messages (strings)”If the best-matching entry is a string, expect-status throws an ExpectStatusError with that message.
await expectStatus(200, response, { 422: "Please check your input.", // throws ExpectStatusError});Message extraction
Section titled “Message extraction”If no dispatch entry matches, expect-status tries to extract a message from the response body. The default extractor checks these shapes in order:
bodyitself (if it’s a non-empty string)body.messagebody.detailorbody.title(RFC 7807)body.errors[0].messageorbody.errors[0](Laravel/DRF)body.error(Spring Boot)
See Message Extraction for customization.
Fallback message
Section titled “Fallback message”If extraction returns nothing, the fallbackMessage is used. The default is 'Request failed with an unexpected status.'.
onError
Section titled “onError”onError fires once with the resolved error, just before it’s thrown (and before recover). It’s for side-effects only — it doesn’t change the result. Errors thrown inside onError are swallowed.
const expectStatus = createExpectStatus({ onError: (err, response) => { Sentry.captureException(err, { extra: { status: response.status } }); },});See Observability Hooks for more.
Recover
Section titled “Recover”recover wraps the entire error path — it catches throws from handlers, message errors, and fallback errors. If it returns a non-undefined value, that becomes the result instead of throwing. If it returns undefined, the error is re-thrown.
const config = await expectStatus(200, api.getFeatureFlags(), { recover: () => DEFAULT_FLAGS, // never throws});See Recover & Transform for details.
Next steps
Section titled “Next steps”- Safe Results —
throws: falsefor structured error handling - Message Extraction — customize how messages are pulled from bodies
- Recover & Transform — catch-all and success body reshaping