sindresorhus/p-retry
sindresorhus/p-retry has 1,040 GitHub stars and 93 forks. GitHot measured 0 stars in 24 hours. Last checked 2026-11-09 11:44 UTC. Last push 2026-11-02.
Retry a promise-returning or async function
Measured star growth
- in 24h
- 0
- in 7 days
- Update delayed
- in 15 days
- Update delayed
- in 1 month
- Update delayed
Net star changes over overlapping periods, not daily totals. Missing measurements are left blank. Stars measure attention, not software quality.
README.md
Originalp-retry
Retry a promise-returning or async function
It does exponential backoff and supports custom retry strategies for failed operations.
Install
npm install p-retry
Usage
import pRetry, {AbortError} from 'p-retry';
const run = async () => {
const response = await fetch('https://sindresorhus.com/unicorn');
// Abort retrying if the resource doesn't exist
if (response.status === 404) {
throw new AbortError(response.statusText);
}
return response.blob();
};
console.log(await pRetry(run, {retries: 5}));
API
pRetry(input, options?)
Returns a Promise that is fulfilled when calling input returns a fulfilled promise. If calling input returns a rejected promise, input is called again until the max retries are reached, it then rejects with the last rejection reason.
Does not retry on most TypeErrors, with the exception of network errors. This is done on a best case basis as different browsers have different messages to indicate this. See whatwg/fetch#526 (comment)
Non-network TypeErrors always abort retries, even if shouldConsumeRetry or shouldRetry would otherwise allow another attempt.
input
Type: Function
Receives the number of attempts as the first argument and is expected to return a Promise or any value.
options
Type: object
onFailedAttempt(context)
Type: Function
Callback invoked on each failure. Receives a context object containing the error and retry state information.
The function is called after shouldConsumeRetry and before shouldRetry, for all errors except AbortError and an abort through signal.
If the function throws, all retries will be aborted and the original promise will reject with the thrown error.
import pRetry from 'p-retry';
const run = async () => {
const response = await fetch('https://sindresorhus.com/unicorn');
if (!response.ok) {
throw new Error(response.statusText);
}
return response.json();
};
const result = await pRetry(run, {
onFailedAttempt: ({error, attemptNumber, retriesLeft, retriesConsumed, retryDelay}) => {
console.log(`Attempt ${attemptNumber} failed. Retrying in ${retryDelay}ms. ${retriesLeft} retries left.`);
// 1st request => Attempt 1 failed. Retrying in 1000ms. 5 retries left.
// 2nd request => Attempt 2 failed. Retrying in 2000ms. 4 retries left.
// …
},
retries: 5
});
console.log(result);
The context object contains:
error- The error that was thrownattemptNumber- The attempt number (starts at 1)retriesLeft- Number of retries remainingretriesConsumed- Number of retries consumed so farretryDelay- The delay in milliseconds before the next retry (based onminTimeout,factor,maxTimeout, andrandomize). The actual delay may be shorter if it would exceedmaxRetryTime. This is0when no retry will occur based on the checks completed before the current callback runs.shouldConsumeRetryis the exception: it is shown the delay that would be used, since deciding whether to consume the retry is what it is being asked.
The onFailedAttempt function can return a promise. For example, to add a delay:
import pRetry from 'p-retry';
import delay from 'delay';
const run = async () => { … };
const result = await pRetry(run, {
onFailedAttempt: async () => {
console.log('Waiting for 1 second before retrying');
await delay(1000);
}
});
shouldRetry(context)
Type: Function
Decide if a retry should occur based on context. Returning true triggers a retry, false aborts with the error. Returning nothing counts as false.
The function is called after onFailedAttempt and shouldConsumeRetry.
The function is not called on AbortError, on an abort through signal, on TypeError (except network errors), or if retries or maxRetryTime are exhausted.
If the function throws, all retries will be aborted and the original promise will reject with the thrown error.
import pRetry from 'p-retry';
const run = async () => { … };
const result = await pRetry(run, {
shouldRetry: ({error, attemptNumber, retriesLeft}) => !(error instanceof CustomError)
});
In the example above, the operation will be retried unless the error is an instance of CustomError.
shouldConsumeRetry(context)
Type: Function
Decide if this failure should consume a retry from the retries budget.
When false is returned, the failure will not consume a retry or increment backoff values, but is still subject to maxRetryTime. Returning nothing counts as true, the default, so retries stays the bound even if a callback forgets its return.
The retry is attempted straight away rather than after a delay, since no delay was accumulated.
The function is called before onFailedAttempt and shouldRetry.
The function is not called on AbortError or when the operation is aborted through signal.
If the function throws, all retries will be aborted and the original promise will reject with the thrown error.
import pRetry from 'p-retry';
const run = async () => { … };
const result = await pRetry(run, {
retries: 2,
shouldConsumeRetry: ({error, retriesLeft}) => !(error instanceof RateLimitError),
maxRetryTime: 60_000,
});
In the example above, RateLimitErrors will not decrement the available retries, so maxRetryTime is what ends the run if the rate limit never clears.
[!WARNING] Failures that do not consume a retry also do not count towards
retries, so returningfalseon every failure meanspRetrynever runs out of attempts.maxRetryTimeis then the only bound, and it defaults toInfinity. Pair this option withmaxRetryTime, or returnfalseonly for failures you expect to stop eventually. These retries also have no delay, so an operation that fails without waiting on a timer or I/O runs back to back without yielding to the event loop, and asignalaborted from a timer or an event cannot stop it.
retries
Type: number
Default: 10
The maximum amount of times to retry the operation. Must be a non-negative integer or Infinity.
factor
Type: number
Default: 2
The exponential factor to use.
A factor of 0 is treated as 1, which keeps the delay constant. A factor below 1 shrinks the delay on every retry until it rounds to 0, and from then on no timer is scheduled. See the warning on minTimeout.
minTimeout
Type: number
Default: 1000
The number of milliseconds before starting the first retry.
Set this to 0 to retry immediately with no delay.
A delay is a whole number of milliseconds, so a minTimeout below 0.5 makes the first delay 0 and schedules no timer for it. randomize scales the delay up by between 1 and 2, and a factor above 1 grows later delays, so either can push a delay back above the threshold.
[!WARNING] A delay of
0schedules no timer at all. If the operation also fails without waiting on a timer or I/O, attempts run back to back without ever yielding to the event loop. Combined withretries: Infinity, that is a busy loop that keeps the CPU pinned and stops timers, I/O, andsignalfrom ever running. Use a small non-zero value if the operation can fail indefinitely.
maxTimeout
Type: number
Default: Infinity
The maximum number of milliseconds between two retries.
Delays are also capped at the largest value a timer can hold, 2147483647ms, about 24.9 days. A longer one is clamped to that rather than firing straight away.
A fractional maxTimeout rounds up to a whole millisecond. A maxTimeout of 0 schedules no timer, like minTimeout: 0. See the warning on minTimeout.
randomize
Type: boolean
Default: false
Randomizes the timeouts by multiplying with a factor between 1 and 2.
maxRetryTime
Type: number
Default: Infinity
The maximum time (in milliseconds) that retrying is allowed to take.
Measured with a monotonic clock (performance.now()) so system clock adjustments do not affect the limit.
The limit is checked after failed attempts and callbacks, before scheduling a retry. The retry delay is shortened to the remaining time, but the scheduled retry can still start after the limit. Attempts and callbacks are never interrupted, so they can run past the limit.
signal
Type: AbortSignal
You can abort retrying using AbortController.
Once the signal is aborted, the abort reason is what the promise rejects with, whatever the operation threw and whatever the callbacks returned. A cancellation is the caller's decision and is not overwritten by a later failure. If the signal is already aborted, input is never called.
An attempt that is already running is not interrupted, so the promise rejects when it settles. Pass the signal to the operation as well if the operation itself should be cancelled.
The exception is a callback that throws or rejects: the boundary checks sit between the callbacks, so an error raised inside one is reported rather than replaced. An abort that lands before the callback is reached still wins.
import pRetry from 'p-retry';
const run = async () => { … };
const controller = new AbortController();
cancelButton.addEventListener('click', () => {
controller.abort(new Error('User clicked cancel button'));
});
try {
await pRetry(run, {signal: controller.signal});
} catch (error) {
console.log(error.message);
//=> 'User clicked cancel button'
}
unref
Type: boolean
Default: false
Prevents retry timeouts from keeping the process alive.
Only affects platforms with a .unref() method on timeouts, such as Node.js.
makeRetriable(function, options?)
Wrap a function so that each call is automatically retried on failure.
The wrapped function is called with the this the wrapper was called with, so a function that declares a this type keeps it.
import {makeRetriable} from 'p-retry';
const fetchWithRetry = makeRetriable(fetch, {retries: 5});
const response = await fetchWithRetry('https://sindresorhus.com/unicorn');
AbortError(message)
AbortError(error)
Throw this from input to abort retrying and reject the promise. No callback functions will be called.
When input throws an AbortError, the promise rejects with the wrapped error. When you pass an error, a catch sees that error. When you pass a message, it sees a plain Error with that message. An AbortError thrown by a callback or passed as a signal abort reason is preserved as is.
message
Type: string
An error message. Optional.
error
Type: Error
A custom error.
Tip
You can pass arguments to the function being retried by wrapping it in an inline arrow function:
import pRetry from 'p-retry';
const run = async emoji => {
// …
};
// Without arguments
await pRetry(run, {retries: 5});
// With arguments
await pRetry(() => run('🦄'), {retries: 5});
FAQ
How do I mock timers when testing with this package?
The package uses setTimeout and clearTimeout from the global scope, so you can use the Node.js test timer mocking or a package like sinon.
maxRetryTime is measured separately, with performance.now(). A fake timer that only replaces setTimeout leaves that clock running, so maxRetryTime is not reached while the mocked clock is. Fake performance.now() too if you are testing that option.
How do I stop retries when the process receives SIGINT (Ctrl+C)?
Use an AbortController to signal cancellation on SIGINT, and pass its signal to pRetry:
import pRetry from 'p-retry';
const run = async () => { … };
const controller = new AbortController();
process.once('SIGINT', () => {
controller.abort(new Error('SIGINT received'));
});
try {
await pRetry(run, {signal: controller.signal});
} catch (error) {
console.log('Retry stopped due to:', error.message);
}
The package does not handle process signals itself to avoid global side effects.
Related
Explore related projects
codecrafters-io/build-your-own-x
Master programming by recreating your favorite technologies from scratch.
561,398 starspublic-apis/public-apis
A collective list of free APIs
497,679 starsmorluto/rea
Reverse engineer anything with agents, from app behavior down to native binaries.
447,495 starsopenclaw/openclaw
The AI that really does things. Any OS. Any Platform. The lobster way. 🦞
394,261 stars