Skip to content

Error Resolution

When the response status doesn’t match your expected status, expect-status resolves an error through a predictable priority chain.

1. Status matches expected → onSuccess → transform → return
2. 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. fallbackMessage
8. onError fires
9. recover → return or re-throw

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

HookPurposeFires when
onSuccessObserve success (analytics, logging)Every success, before transform
onErrorObserve errors (logging, metrics)Every error, before recover
transformReshape success bodySuccess path, after onSuccess
recoverCatch errors, return instead of throwError path (true catch-all)

When the status matches the expected status, expect-status:

  1. Calls onSuccess(response) — per-call hook shadows instance hook
  2. Calls transform(body) if provided — reshapes the body before returning
  3. Returns the (optionally transformed) body
await expectStatus(200, response, {
onSuccess: (res) => analytics.track(res.status),
transform: (body) => ({ data: body, ts: Date.now() }),
});

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

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

If no dispatch entry matches, expect-status tries to extract a message from the response body. The default extractor checks these shapes in order:

  1. body itself (if it’s a non-empty string)
  2. body.message
  3. body.detail or body.title (RFC 7807)
  4. body.errors[0].message or body.errors[0] (Laravel/DRF)
  5. body.error (Spring Boot)

See Message Extraction for customization.

If extraction returns nothing, the fallbackMessage is used. The default is 'Request failed with an unexpected status.'.

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