timeout
withTimeout(ms) gives every attempt its own deadline.
import { corgi } from '@itsy/corgi';
import { withTimeout } from '@itsy/corgi/timeout';
const api = corgi.create({ plugins: [withTimeout(5000)] });
await api.get('/slow'); // rejects with a TimeoutError after 5sLike any plugin it also works via /chonk's options and around native fetch.
Picking an implementation
It ships twice. Both expose the same withTimeout(ms), the same ordering slot, and the same "TimeoutError" name, so you can swap the import freely.
| import | how | runtime floor | size |
|---|---|---|---|
@itsy/corgi/timeout | hand-rolled setTimeout + AbortController | 2022-safe (all targets) | larger |
@itsy/corgi/timeout-modern | AbortSignal.timeout() + AbortSignal.any() | Baseline 2024 | ~60% smaller |
import { withTimeout } from '@itsy/corgi/timeout-modern';Which one do I want?
Targeting evergreen browsers, Node 18.17+, Deno, Bun, or Workers, and want the smallest bundle? Use @itsy/corgi/timeout-modern.
Supporting older browsers (pre-2024 Safari/Firefox), or want guaranteed timer cleanup under very high load? Use @itsy/corgi/timeout.
Behaviour
- Tagged
ORDER.timeout(innermost). Withretry, each attempt gets a fresh deadline and a fired timeout is retried. - A caller's own
signalis forwarded with its abort reason preserved, so a user-cancel stays anAbortErrorwhile a timeout stays aTimeoutError. msof0orInfinitymeans no timeout; the request is forwarded untouched.- Detect it with
isTimeoutError.
Per-attempt vs total budget
withTimeout is per-attempt. For a single total budget across all retries, pass an AbortSignal.timeout as the request signal instead. When it fires, the retry loop sees the aborted signal and stops.
await api.get('/report', { signal: AbortSignal.timeout(30_000) });Use both together to cap each attempt and the whole call. Whichever fires first wins.
A per-request signal is something you have to remember to pass. To make the total budget a client-level default instead, re-tag a second withTimeout to sit outside retry.