@billdaddy/retryfn
v0.1.1
Published
Retry async functions with exponential backoff, jitter, AbortSignal, per-attempt timeouts, and Retry-After awareness. Zero dependencies.
Maintainers
Readme
retryfn
Retry async functions with exponential backoff, jitter,
AbortSignal, per-attempt timeouts, andRetry-Afterawareness. Zero dependencies.
Transient failures are a fact of life: a dropped connection, a 429 Too Many
Requests from an LLM API, a service that's briefly 503. retryfn retries the
operation the right way — exponential backoff with jitter so you don't stampede,
honoring the server's Retry-After header when it sends one, with real
AbortSignal and per-attempt timeout support.
import { retry } from "@billdaddy/retryfn";
const data = await retry(
async ({ signal }) => {
const res = await fetch(url, { signal });
if (res.status === 429 || res.status >= 500) {
throw Object.assign(new Error(`HTTP ${res.status}`), { response: res });
}
return res.json();
},
{ retries: 5, timeout: 10_000 },
);If that 429 carried Retry-After: 2, the next attempt waits exactly 2 seconds —
not a guessed backoff.
Why retryfn?
- Server-aware. Reads a
Retry-Afterhint (seconds or HTTP-date, from aHeadersobject or a plain record) and waits exactly that long. - Cancellable. Each attempt gets an
AbortSignaldriven by yourtimeoutand your own externalsignal— forward it straight tofetch. - Good backoff by default. Exponential growth with full jitter, capped by
maxDelayand an optional totalmaxElapsedbudget. - Precise control.
shouldRetry(error)to decide per-error,onRetryto observe, deterministic via an injectablerng. - Zero dependencies, ESM + CJS + types, and a CLI to retry shell commands.
Install
npm install @billdaddy/retryfn
# or: pnpm add @billdaddy/retryfn / yarn add @billdaddy/retryfn / bun add @billdaddy/retryfnAPI
retry(fn, options?) → Promise<T>
fn receives { attempt, signal }. Throw to trigger a retry; return to resolve.
| Option | Type | Default | Description |
| ----------------- | -------------------------------------- | -------- | ------------------------------------------------- |
| retries | number | 3 | Retries after the first try (4 attempts total). |
| minDelay | number (ms) | 200 | Base delay for the first retry. |
| maxDelay | number (ms) | 30000 | Cap on a single computed delay. |
| factor | number | 2 | Exponential multiplier. |
| jitter | "full" \| "equal" \| "none" | "full" | Randomisation strategy. |
| maxElapsed | number (ms) | — | Total time budget across all attempts/waits. |
| timeout | number (ms) | — | Per-attempt timeout (aborts the attempt signal). |
| signal | AbortSignal | — | Cancel the whole operation. |
| honorRetryAfter | boolean | true | Prefer a Retry-After hint over backoff. |
| shouldRetry | (error, attempt) => boolean \| Promise | retry all | Decide whether an error is retryable. |
| onRetry | ({error, attempt, delay}) => void | — | Observe each scheduled retry. |
Throws the last error when retries are exhausted, shouldRetry returns false,
or the external signal aborts.
calcBackoff(attempt, options?, rng?) → number
The backoff math on its own (zero-based attempt), exported for reuse and testing.
getRetryAfterMs(error, now?) → number | undefined
Extract a Retry-After wait (ms) from an error: error.retryAfterMs,
error.retryAfter (seconds), or a Retry-After header on
error.response.headers / error.headers.
isAbortError(err) → boolean
true for AbortError / TimeoutError thrown via an AbortSignal.
Recipes
Only retry network/5xx, never 4xx (except 429):
await retry(call, {
shouldRetry: (err) => {
const s = (err as any).response?.status;
return s == null || s === 429 || s >= 500;
},
});Hard ceiling on total time:
await retry(call, { retries: 20, maxElapsed: 15_000 });CLI
Retry a shell command until it succeeds:
retryfn -r 5 -- curl -fsS https://flaky.example.com/health
retryfn --min 1000 --factor 3 -- ./deploy.shContributors ✨
This project follows the all-contributors specification. Contributions of any kind are welcome — code, docs, bug reports, ideas, reviews! See the emoji key for how each contribution is recognized, and open a PR or issue to get involved.
Thanks goes to these wonderful people:
License
MIT © Tung Tran
