async-retry vs backoff vs exponential-backoff vs p-retry vs promise-retry vs retry
Implementing Resilient Retry Logic in JavaScript Applications
async-retrybackoffexponential-backoffp-retrypromise-retryretrySimilar Packages:

Implementing Resilient Retry Logic in JavaScript Applications

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.

Npm Package Weekly Downloads Trend

3 Years

Github Stars Ranking

Stat Detail

Package
Downloads
Stars
Size
Issues
Publish
License
async-retry01,913-305 years agoMIT
backoff0336-1110 years agoMIT
exponential-backoff040555.2 kB66 months agoApache-2.0
p-retry01,00525.5 kB123 days agoMIT
promise-retry0318-116 years agoMIT
retry01,259-205 years agoMIT

Implementing Resilient Retry Logic: A Deep Dive into 6 JavaScript Libraries

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.

🏗️ API Style: Callbacks vs. Promises

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.

  • You pass a function and a callback.
  • The callback receives an error if all retries fail.
  • Harder to read in modern codebases.
// 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.

  • You create a backoff instance and listen for a backoff event.
  • You manually trigger the next attempt inside the event handler.
  • Gives you control but requires more boilerplate.
// 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.

  • Returns a Promise that resolves with the result or rejects after max attempts.
  • Clean 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.

  • Maintained by Vercel, designed for serverless functions.
  • Very concise syntax, passes a 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.

  • Returns a Promise.
  • Allows you to filter which errors should trigger a retry based on error codes.
// 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.

  • Provides a backOff function that wraps your async operation.
  • Highly configurable timing, less focus on error filtering logic.
// exponential-backoff: Promise-based
import { backOff } from 'exponential-backoff';

const result = await backOff(async () => {
  return await fetchData();
}, {
  numOfAttempts: 3
});

⏱️ Backoff Strategies: How Long to Wait

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.

  • You define retries, factor, minTimeout, and maxTimeout.
  • Good balance of control and simplicity.
// retry: Custom timeouts
const operation = retry.operation({
  retries: 3,
  factor: 2,
  minTimeout: 1000,
  maxTimeout: 5000
});

backoff allows you to swap strategies entirely.

  • Comes with ExponentialStrategy, FibonacciStrategy, and custom implementations.
  • Best if you need non-standard timing (like Fibonacci).
// backoff: Pluggable strategies
const call = backoff.call(fn);
call.setStrategy(new backoff.FibonacciStrategy());

exponential-backoff is the most strict about the algorithm.

  • Supports jitter (randomness) out of the box to prevent thundering herd problems.
  • Ideal for high-traffic APIs where many clients might retry at once.
// 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.

  • They focus more on the flow control than the math.
  • Usually sufficient for standard web requests.
// p-retry: Standard options
await pRetry(fn, {
  retries: 3,
  factor: 2,
  minTimeout: 1000
});

🛑 Stopping Early: When to Give Up

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.

  • Pass bail as an argument to your async function.
  • Call bail(error) to stop retries and reject immediately.
  • Very clear intent in code.
// 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.

  • You can throw an error that matches a filter to stop.
  • Or use the 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.

  • Checks err.code against a list of retryable errors.
  • If the code isn't in the list, it stops.
// 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.

  • You must check the error inside your operation function.
  • If it's fatal, you call the final callback or throw without triggering the next backoff.
// 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;
    }
  });
});

🛠️ Maintenance and Ecosystem

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 retry and backoff for new frontend projects. The callback pattern makes code harder to read and test compared to Promise-based alternatives.

📊 Summary Table

PackageStyleBackoff ControlBail/Stop LogicBest For
async-retryPromiseStandardbail() functionServerless, Modern Node
p-retryPromiseStandard⚠️ Error flagsGeneral Web Apps
exponential-backoffPromise✅ Advanced (Jitter)⚠️ Error throwingHigh-traffic APIs
promise-retryPromiseStandard⚠️ Error codesTooling, npm-like tasks
retryCallbackStandard⚠️ Manual checkLegacy Code
backoffCallback/Event✅ Pluggable Strategies⚠️ Manual checkLow-level Network Tools

💡 Final Recommendation

For most modern frontend and Node.js developers, async-retry or p-retry are the best choices.

  • Pick async-retry if you like the bail pattern for clearly stopping retries on fatal errors. It is concise and well-maintained.
  • Pick p-retry if you want a minimal wrapper that feels like a native Promise utility.
  • Pick 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.

How to Choose: async-retry vs backoff vs exponential-backoff vs p-retry vs promise-retry vs retry

  • async-retry:

    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.

  • backoff:

    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.

  • exponential-backoff:

    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.

  • p-retry:

    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.

  • promise-retry:

    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.

  • retry:

    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.

README for async-retry

async-retry

Retrying made simple, easy, and async.

Usage

// 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,
  }
);

API

retry(retrier : Function, opts : Object) => Promise
  • The supplied function can be async or not. In other words, it can be a function that returns a Promise or a value.
  • The supplied function receives two parameters
    1. A Function you can invoke to abort the retrying (bail)
    2. A Number identifying the attempt. The absolute first attempt (before any retries) is 1.
  • The 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.

Authors