These six libraries provide mechanisms to automatically retry failed operations, such as network requests or database queries, to improve application resilience. While they share the same goal, they differ significantly in their API style (callbacks vs. promises), backoff strategies, and maintenance status. retry is the foundational callback-based library, while async-retry, p-retry, and promise-retry offer modern Promise-based interfaces. backoff and exponential-backoff focus specifically on the timing algorithms behind the retries. Choosing the right one depends on whether your codebase uses async/await, how much control you need over delay timing, and whether you require active maintenance.
Network failures, database locks, and temporary API rate limits are facts of life in distributed systems. To handle them, developers use retry logic — automatically attempting a failed operation again after a short delay. The JavaScript ecosystem offers several tools for this, ranging from low-level backoff algorithms to high-level Promise wrappers. Let's compare async-retry, backoff, exponential-backoff, p-retry, promise-retry, and retry to see which one fits your architecture.
The biggest divide in this group is between older callback-based libraries and modern Promise-based ones. This choice dictates how your code looks and how easily it integrates with async/await.
retry is the original foundation. It uses callbacks exclusively.
// retry: Callback-based
const retry = require('retry');
const operation = retry.operation({ retries: 3 });
operation.attempt(function(currentAttempt) {
fetchData(function(err, result) {
if (operation.retry(err)) {
return;
}
console.error(operation.mainError());
});
});
backoff also leans heavily on callbacks and events.
backoff event.// backoff: Event-based
const backoff = require('backoff');
const call = backoff.call(fetchData, arg1, arg2);
call.setStrategy(new backoff.ExponentialStrategy());
call.on('backoff', function(number, delay) {
console.log(`Waiting ${delay}ms before retry #${number}`);
});
call.on('callback', function(err, result) {
console.log('Done:', err || result);
});
call.start();
p-retry wraps the original retry module in a Promise.
async/await syntax.// p-retry: Promise-based
import pRetry from 'p-retry';
const result = await pRetry(async () => {
return await fetchData();
}, {
retries: 3
});
async-retry is built for Promises from the ground up.
bail function to stop retries early.// async-retry: Promise-based
import retry from 'async-retry';
const result = await retry(async (bail) => {
const res = await fetchData();
if (res.status === 404) {
bail(new Error('Not Found')); // Stop retrying immediately
}
return res;
}, {
retries: 3
});
promise-retry is similar to p-retry but with npm-style error filtering.
// promise-retry: Promise-based
const promiseRetry = require('promise-retry');
const result = await promiseRetry((retry, number) => {
return fetchData().catch(retry);
}, {
retries: 3,
factor: 2
});
exponential-backoff focuses on the timing algorithm but supports Promises.
backOff function that wraps your async operation.// exponential-backoff: Promise-based
import { backOff } from 'exponential-backoff';
const result = await backOff(async () => {
return await fetchData();
}, {
numOfAttempts: 3
});
When a request fails, you shouldn't retry immediately. Waiting longer between each attempt (exponential backoff) prevents overwhelming the server. Some libraries let you customize this; others have fixed defaults.
retry uses a customizable exponential formula.
retries, factor, minTimeout, and maxTimeout.// retry: Custom timeouts
const operation = retry.operation({
retries: 3,
factor: 2,
minTimeout: 1000,
maxTimeout: 5000
});
backoff allows you to swap strategies entirely.
ExponentialStrategy, FibonacciStrategy, and custom implementations.// backoff: Pluggable strategies
const call = backoff.call(fn);
call.setStrategy(new backoff.FibonacciStrategy());
exponential-backoff is the most strict about the algorithm.
jitter (randomness) out of the box to prevent thundering herd problems.// exponential-backoff: Built-in jitter
await backOff(fn, {
jitter: 'full', // Adds randomness to delay
startingDelay: 500,
timeMultiple: 2
});
p-retry, async-retry, and promise-retry inherit or mimic standard exponential defaults.
// p-retry: Standard options
await pRetry(fn, {
retries: 3,
factor: 2,
minTimeout: 1000
});
Sometimes a retry is pointless. If a user is not found (404), retrying won't help. You need a way to break the loop immediately.
async-retry makes this explicit with a bail function.
bail as an argument to your async function.bail(error) to stop retries and reject immediately.// async-retry: Explicit bail
await retry(async (bail) => {
const res = await api.getUser(id);
if (res.status === 404) {
bail(new Error('User missing')); // Stops retries
}
return res;
});
p-retry handles this by throwing specific errors.
shouldRetry option in newer versions to return false.// p-retry: Filter based
await pRetry(async () => {
const res = await api.getUser(id);
if (res.status === 404) {
const err = new Error('User missing');
err.doNotRetry = true; // Custom flag to stop
throw err;
}
return res;
});
promise-retry uses error codes.
err.code against a list of retryable errors.// promise-retry: Code based
await promiseRetry((retry, number) => {
return fetchData().catch(err => {
if (err.code === 'ENOTFOUND') {
throw err; // Won't retry this specific code if filtered
}
retry(err);
});
}, { retries: 3 });
retry, backoff, and exponential-backoff require manual logic.
// retry: Manual check
operation.attempt(function(currentAttempt) {
fetchData(function(err, result) {
if (err && err.code === 'NOT_RETRYABLE') {
// Manually fail without retrying
console.error(err);
return;
}
if (operation.retry(err)) {
return;
}
});
});
Not all libraries are kept up to date. Using an unmaintained package can lead to security risks or incompatibility with newer Node.js versions.
async-retry: Actively maintained by Vercel. Safe for production. Widely used in serverless.p-retry: Actively maintained by Sindre Sorhus. Very stable. Great for frontend and Node.exponential-backoff: Active. Good choice if you need strict backoff compliance.promise-retry: Maintained but more niche. Often seen in npm internals.retry: Stable but older. Callback style is dated.backoff: Less common in modern web apps. More for low-level network tools.⚠️ Warning: Avoid
retryandbackofffor new frontend projects. The callback pattern makes code harder to read and test compared to Promise-based alternatives.
| Package | Style | Backoff Control | Bail/Stop Logic | Best For |
|---|---|---|---|---|
async-retry | Promise | Standard | ✅ bail() function | Serverless, Modern Node |
p-retry | Promise | Standard | ⚠️ Error flags | General Web Apps |
exponential-backoff | Promise | ✅ Advanced (Jitter) | ⚠️ Error throwing | High-traffic APIs |
promise-retry | Promise | Standard | ⚠️ Error codes | Tooling, npm-like tasks |
retry | Callback | Standard | ⚠️ Manual check | Legacy Code |
backoff | Callback/Event | ✅ Pluggable Strategies | ⚠️ Manual check | Low-level Network Tools |
For most modern frontend and Node.js developers, async-retry or p-retry are the best choices.
async-retry if you like the bail pattern for clearly stopping retries on fatal errors. It is concise and well-maintained.p-retry if you want a minimal wrapper that feels like a native Promise utility.exponential-backoff only if you are building a high-scale client where jitter and strict timing are critical to avoid rate limiting.Avoid retry and backoff unless you are working on legacy systems. The Promise-based tools reduce boilerplate and integrate seamlessly with async/await, making your error handling logic easier to follow and maintain.
Choose async-retry if you want a lightweight, Promise-based wrapper that is actively maintained by Vercel. It is ideal for serverless environments or modern Node.js applications where you need simple retry logic with exponential backoff without extra configuration overhead.
Choose backoff if you are working with legacy callback-based code or streams and need fine-grained control over the backoff strategy itself. It is less suitable for new Promise-based projects but remains useful for low-level network tools or when you need to separate the backoff logic from the retry execution.
Choose exponential-backoff if your primary concern is a highly configurable, standards-compliant exponential backoff algorithm with jitter. It is best for scenarios where network congestion is a major concern and you need to adhere to specific retry-after headers or strict timing requirements.
Choose p-retry if you are already using the Sindre Sorhus ecosystem or prefer a minimal, focused Promise wrapper around the original retry module. It is excellent for frontend or Node.js projects that need a simple await interface with sensible defaults and abort signal support.
Choose promise-retry if you need compatibility with npm-style retry logic or require specific handling for error codes (like ENOTFOUND). It is often used in tooling or package managers but may be overkill for standard application HTTP requests.
Choose retry only if you are maintaining legacy code that relies on callbacks or if you need the absolute base layer upon which other libraries are built. For new projects, prefer a Promise-based alternative like p-retry or async-retry to avoid callback hell.
Retrying made simple, easy, and async.
// Packages
const retry = require('async-retry');
const fetch = require('node-fetch');
await retry(
async (bail) => {
// if anything throws, we retry
const res = await fetch('https://google.com');
if (403 === res.status) {
// don't retry upon 403
bail(new Error('Unauthorized'));
return;
}
const data = await res.text();
return data.substr(0, 500);
},
{
retries: 5,
}
);
retry(retrier : Function, opts : Object) => Promise
async or not. In other words, it can be a function that returns a Promise or a value.Function you can invoke to abort the retrying (bail)Number identifying the attempt. The absolute first attempt (before any retries) is 1.opts are passed to node-retry. Read its docs
retries: The maximum amount of times to retry the operation. Default is 10.factor: The exponential factor to use. Default is 2.minTimeout: The number of milliseconds before starting the first retry. Default is 1000.maxTimeout: The maximum number of milliseconds between two retries. Default is Infinity.randomize: Randomizes the timeouts by multiplying with a factor between 1 to 2. Default is true.onRetry: an optional Function that is invoked after a new retry is performed. It's passed the Error that triggered it as a parameter.