p-retry review
p-retry repeatedly calls a promise-returning function until it succeeds, a retry policy rejects the failure, an abort signal fires, or its attempt or time budget ends. It supplies exponential delay, jitter, retry classification, failures that do not consume the attempt budget, and makeRetriable() for wrapping an existing function. Version 8 requires Node 22, exposes the calculated retryDelay to failure callbacks, changes callback order so budget consumption is decided first, and tightens handling of TypeError, timing options, and callback validation.
p-retry is a good small policy layer for idempotent async work when Node 22 is acceptable and your code can classify failures. Skip it when retries require shared circuit state, exact server delay handling, or protection against duplicated side effects.
We installed it
| Install | ✓ · 0.6s | 2 packages on disk · 1 MB |
| Import | ✓ | ESM import works · require() works · ESM package with exports map |
| Browser | 1.8 KB | gzipped (4.4 KB minified), bundled with esbuild |
| Types | ✓ | TypeScript types bundled |
| Known vulns | 0 | 0 critical · 0 high · 0 moderate · 0 low (npm audit) |
Answers from our run
Does p-retry install cleanly?
Yes. In a fresh container with an empty cache, npm install p-retry finished in 0.6s, leaving 2 packages and 1 MB on disk. npm audit reported no known vulnerabilities.
How much does p-retry add to a browser bundle?
1.8 KB gzipped (4.4 KB minified) when the whole package is bundled for the browser with esbuild. Importing only part of it is usually smaller.
Does p-retry work with both ESM and CommonJS?
Yes. Both import 'p-retry' and require('p-retry') worked in Node 22 in our run. The package is published as ESM with an exports map.
Does p-retry include TypeScript types?
Yes, type declarations ship inside the package, so no @types install is needed.
p-retry or async-retry: which should you use?
async-retry: Use it for a compact bail-and-retry API on older Node versions. p-retry is a good small policy layer for idempotent async work when Node 22 is acceptable and your code can classify failures.
When should you not use p-retry?
The operation is not idempotent and has no idempotency key; replaying a payment, message, or mutation can duplicate real work
Use it if
- A network or distributed-system operation fails transiently and the caller owns a clear retry policy
- You need exponential delay, jitter, an elapsed-time ceiling, and AbortSignal support without writing a scheduling loop
- Rate-limit failures should wait and retry without consuming the same budget used for ordinary transient errors
- Several calls to one async function should share a tested policy through makeRetriable()
- The operation is not idempotent and has no idempotency key; replaying a payment, message, or mutation can duplicate real work
- Your runtime is below Node 22, which version 8 names as its engine floor
- You expect HTTP status handling out of the box; fetch resolves on 4xx and 5xx, so your function must inspect the response and throw a classified error
- A retry must obey Retry-After exactly; p-retry calculates local delays, so server-provided timing needs custom waiting logic
- You need a circuit breaker or shared concurrency control across calls; this package schedules one invocation and has no service-wide failure state
Setup reality
Our fresh Node 22 install of p-retry 8.0.0 completed in 0.6 seconds. Two packages occupied 1 MB afterward, and npm audit reported zero known vulnerabilities. p-retry declares one direct dependency, no peers, and a Node 22 minimum. Its own unpacked package measured 40 KB. TypeScript declarations are bundled. It declares ESM with an exports map, while both import and require() worked in our sandbox. A complete browser import measured 4.4 KB minified and 1.8 KB gzipped.
There are no credentials or config files. The input function receives an attempt number and must throw or reject on failure. fetch does not reject for HTTP error statuses, so check response.ok and throw an error that shouldRetry can classify. AbortError skips all callbacks and ends immediately. Other TypeError failures normally stop as programming errors; the package makes a best-effort exception for recognized network TypeErrors.
The default is ten retries after the initial call, with exponential delay starting at one second. Set retries, minTimeout, maxTimeout, factor, randomize, and maxRetryTime from the operation's real deadline. randomize adds jitter but does not read Retry-After. Version 8 calls shouldConsumeRetry before onFailedAttempt and shouldRetry, and the callback context now includes retryDelay. Code written around the old order must be reviewed during upgrade.
Cancellation stops the retry schedule through AbortSignal, but the running operation also needs the signal if it should stop mid-attempt. The library does not install SIGINT handlers. In short-lived Node commands, unref prevents a pending delay timer from keeping the process alive. A failure excluded by shouldConsumeRetry still counts against maxRetryTime, which is the safety boundary for rate-limit loops.
Patterns
Retry a failing async function retry-async-operation
import pRetry from 'p-retry';
const value = await pRetry(
attempt => readRemoteValue({ attempt }),
{ retries: 4 },
);retries counts repeats after the first attempt, so this configuration permits five calls.
Throw on retryable HTTP responses classify-http-status
import pRetry, { AbortError } from 'p-retry';
const data = await pRetry(async () => {
const response = await fetch(url);
if (response.status === 429 || response.status >= 500) {
throw new Error(`temporary HTTP ${response.status}`);
}
if (!response.ok) throw new AbortError(`HTTP ${response.status}`);
return response.json();
});fetch resolves for HTTP errors. AbortError ends without running retry callbacks.
Retry only selected failures filter-retry-errors
await pRetry(run, {
shouldRetry: ({ error }) =>
error instanceof NetworkError || error instanceof TimeoutError,
});shouldRetry runs after budget consumption and onFailedAttempt in version 8.
Keep rate limits outside the retry count exclude-rate-limit-budget
await pRetry(run, {
retries: 3,
maxRetryTime: 30_000,
shouldConsumeRetry: ({ error }) => !(error instanceof RateLimitError),
});Excluded failures still consume elapsed time, so keep maxRetryTime finite.
Report the next scheduled delay log-retry-delay
await pRetry(run, {
onFailedAttempt: ({ error, attemptNumber, retriesLeft, retryDelay }) => {
console.warn({ error, attemptNumber, retriesLeft, retryDelay });
},
});retryDelay is new in version 8 and is zero when no retry will be scheduled.
Bound exponential backoff set-backoff-policy
await pRetry(run, {
retries: 6,
factor: 2,
minTimeout: 250,
maxTimeout: 5_000,
randomize: true,
maxRetryTime: 20_000,
});maxRetryTime uses a monotonic clock and caps the whole retry operation.
Cancel waiting and active work cancel-retry-loop
const controller = new AbortController();
process.once('SIGINT', () => controller.abort(new Error('SIGINT')));
await pRetry(
() => fetch(url, { signal: controller.signal }),
{ signal: controller.signal },
);Pass the signal to pRetry and the operation; the package does not register process handlers itself.
Apply one policy to every call wrap-retriable-function
import { makeRetriable } from 'p-retry';
const fetchWithRetry = makeRetriable(fetch, {
retries: 3,
maxRetryTime: 10_000,
});
const response = await fetchWithRetry(url, options);Arguments and return types follow the wrapped function, but HTTP status classification still belongs in that function.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| async-retry | npm | Use it for a compact bail-and-retry API on older Node versions |
| promise-retry | npm | Use it when existing code already follows the retry module's operation model |
| p-timeout | npm | Use it when the requirement is one deadline without replaying the operation |
More utils guides
lru-cache · ajv · type-fest · p-limit · find-up · js-yaml · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

