Tiny retry with exponential backoff, jitter, timeout, and AbortSignal support.
- Zero dependencies
- TypeScript-first with full type inference
- ~220 lines of code (plus JSDoc)
- ESM and CommonJS support
Your OpenAI call hangs. Your Stripe webhook times out. You add p-retry but:
// p-retry: no per-attempt timeout, so one hung request = stuck forever
// p-retry: jitter is OFF by default, so your retries stampede together
// p-retry: you throw AbortError to stop retrying (weird)import { retry } from "nano-retry";
await retry(() => openai.chat.completions.create({ model: "gpt-4", messages }), {
attemptTimeout: 30000, // kill hung requests after 30s (p-retry can't do this)
retryIf: (err) => err.status === 429 || err.status >= 500, // simple predicate
});
// jitter already on by default - no thundering herdThat's it. Your call retries on failure, times out stuck attempts, and staggers retries automatically.
| Feature | p-retry | async-retry | nano-retry |
|---|---|---|---|
| Dependencies | 2 | 1 | 0 |
| Native TypeScript | ✅ | ❌ (needs @types) | ✅ |
| ESM + CJS | ESM-only (v6+) | CJS only | ✅ Both |
| Per-attempt timeout | ❌ | ❌ | ✅ |
| Total timeout | ✅ | ❌ | ✅ |
| AbortSignal | ✅ | ❌ | ✅ |
| Jitter (default) | opt-in | opt-in | ✅ on by default |
| Bail mechanism | AbortError class | bail() callback | retryIf predicate |
| Bundle size (gzip) | ~3KB + deps | ~2KB + deps | 2.3KB |
npm install nano-retryRequirements: Node.js 16+ or modern browsers (ES2020)
import { retry } from "nano-retry";
// Retries 3 times with exponential backoff (1s, 2s, 4s)
const data = await retry(() => fetch("/api/data"));
// With options
await retry(() => callApi(), {
retries: 5, // max 5 retries
attemptTimeout: 10000, // 10s max per attempt
retryIf: (err) => err.status >= 500, // only retry server errors
onRetry: (err, ctx) => console.log(`Retry ${ctx.attempt}...`),
});Retry an async function with exponential backoff.
const result = await retry(
(attempt) => fetchData(), // attempt is 1-based
{ retries: 3 }
);| Option | Type | Default | Description |
|---|---|---|---|
retries |
number |
3 |
Maximum retry attempts (not counting initial) |
minTimeout |
number |
1000 |
Initial delay in ms |
maxTimeout |
number |
30000 |
Maximum delay in ms |
factor |
number |
2 |
Exponential backoff multiplier |
jitter |
boolean |
true |
Add ±25% randomization to delays |
attemptTimeout |
number |
- | Timeout for each attempt in ms |
totalTimeout |
number |
- | Max total time for all attempts in ms |
signal |
AbortSignal |
- | Cancel the retry operation |
retryIf |
(error, ctx) => boolean |
- | Return false to stop retrying |
onRetry |
(error, ctx) => void |
- | Called before each retry |
The retryIf and onRetry callbacks receive a context object:
interface RetryContext {
attempt: number; // Current attempt (1-based)
retriesLeft: number; // Remaining retries
elapsed: number; // Total elapsed time in ms
nextDelay: number; // Delay before next retry
}Create a pre-configured retryable function.
const fetchWithRetry = retryable(
(url: string) => fetch(url),
{ retries: 3, minTimeout: 500 }
);
const data = await fetchWithRetry("/api/users");Thrown when the operation is cancelled via AbortSignal.
import { AbortError } from "nano-retry";
try {
await retry(fn, { signal: controller.signal });
} catch (error) {
if (error instanceof AbortError) {
console.log("Operation was cancelled");
}
}Thrown when attemptTimeout or totalTimeout is exceeded.
import { TimeoutError } from "nano-retry";
try {
await retry(fn, { attemptTimeout: 5000 });
} catch (error) {
if (error instanceof TimeoutError) {
console.log("Operation timed out");
}
}const result = await retry(
() => callApi(),
{
retries: 5,
minTimeout: 1000,
retryIf: (error) => error.status === 429,
}
);await retry(fetchData, {
retryIf: (error) => {
// Retry 5xx and network errors, not 4xx
if (error.status >= 500) return true;
if (error.code === "ECONNRESET") return true;
return false;
},
});// Each attempt has 5 seconds to complete
await retry(
() => slowOperation(),
{
retries: 3,
attemptTimeout: 5000,
}
);// Entire operation must complete within 30 seconds
await retry(
() => fetchData(),
{
retries: 10,
totalTimeout: 30000,
}
);const controller = new AbortController();
// Cancel after 10 seconds
setTimeout(() => controller.abort(), 10000);
try {
await retry(fetchData, { signal: controller.signal });
} catch (error) {
if (error.name === "AbortError") {
console.log("Cancelled");
}
}await retry(fetchData, {
onRetry: (error, ctx) => {
console.log(
`Attempt ${ctx.attempt} failed: ${error.message}. ` +
`Retrying in ${ctx.nextDelay}ms (${ctx.retriesLeft} left)`
);
},
});const RETRYABLE_CODES = new Set(["40001", "40P01", "ECONNRESET"]);
await retry(
() => db.query("SELECT * FROM users"),
{
retries: 3,
minTimeout: 100,
retryIf: (error) => RETRYABLE_CODES.has(error.code),
}
);Full type inference is supported:
import { retry, RetryOptions, RetryContext } from "nano-retry";
interface User {
id: string;
name: string;
}
// Return type is inferred as Promise<User>
const user = await retry(async (): Promise<User> => {
const res = await fetch("/api/user");
return res.json();
});nano-retry is optimized for the common case (no retries needed):
| Operation | Time |
|---|---|
| Direct call (baseline) | 0.10 µs |
| retry() - success | 0.22 µs |
| Overhead | 0.12 µs |
The library adds minimal overhead when your function succeeds on the first attempt.
p-retry requires importing and throwing a special AbortError:
// p-retry
import pRetry, { AbortError } from "p-retry";
await pRetry(async () => {
const error = await getError();
if (error.status === 400) {
throw new AbortError("Bad request"); // Stop retrying
}
});nano-retry uses a simple predicate:
// nano-retry
await retry(getError, {
retryIf: (error) => error.status !== 400, // false = stop
});p-retry doesn't support per-attempt timeouts. You need to handle it yourself:
// p-retry - manual timeout
await pRetry(async () => {
const controller = new AbortController();
setTimeout(() => controller.abort(), 5000);
return fetch(url, { signal: controller.signal });
});nano-retry has built-in support:
// nano-retry - built-in
await retry(() => fetch(url), { attemptTimeout: 5000 });npm install
npm run build # Compile TypeScript (CJS + ESM)
npm test # Run tests (45 tests)
npm run bench # Run benchmarks
npm run verify # Run all checksMIT