async-retry vs backoff vs exponential-backoff vs promise-retry vs retry
Implementing Resilient Retry Logic in JavaScript
async-retrybackoffexponential-backoffpromise-retryretrySimilar Packages:

Implementing Resilient Retry Logic in JavaScript

Transient failures are a fact of life in distributed systems and web applications. Network requests fail, databases lock, and APIs rate-limit. These five packages provide strategies to handle such failures by automatically retrying operations. They differ primarily in their age, programming model (callbacks vs promises), and configuration flexibility. Choosing the right one depends on whether you are working in a legacy callback environment or a modern async/await codebase, and how much control you need over the backoff strategy.

Npm Package Weekly Downloads Trend

3 Years

Github Stars Ranking

Stat Detail

Package
Downloads
Stars
Size
Issues
Publish
License
async-retry01,918-305 years agoMIT
backoff0336-1110 years agoMIT
exponential-backoff040555.2 kB68 months agoApache-2.0
promise-retry0320-116 years agoMIT
retry01,258-215 years agoMIT

Implementing Resilient Retry Logic in JavaScript

When building robust applications, handling transient failures is non-negotiable. Whether you are fetching data from an API or writing to a database, things will fail temporarily. The packages async-retry, backoff, exponential-backoff, promise-retry, and retry all solve this problem, but they approach it from different eras of JavaScript development. Let's compare how they handle execution, configuration, and errors.

πŸ”„ Execution Model: Callbacks vs Promises

The biggest divide in this group is between older callback-based tools and modern promise-based ones. This dictates how they fit into your codebase.

retry is the original. It uses callbacks for both the operation and the completion.

const retry = require('retry');

const operation = retry.operation({ retries: 3 });

operation.attempt(function(currentAttempt) {
  fetchData(function(err, result) {
    if (operation.retry(err)) {
      return;
    }
    console.log(operation.mainError());
  });
});

backoff also relies on callbacks, focusing on the backoff strategy itself.

const backoff = require('backoff');

const fibBackoff = backoff.fibonacci();

fibBackoff.failAfter(3);

fibBackoff.on('backoff', function(number, delay) {
  // Try operation here after delay
});

fibBackoff.backoff();

promise-retry wraps the logic to return a promise, bridging the gap.

const promiseRetry = require('promise-retry');

promiseRetry(function(retry, number) {
  return fetchData()
    .catch(function(err) {
      if (retry.isError(err)) {
        throw err; // Triggers retry
      }
      throw err; // Fails immediately
    });
}, { retries: 3 })
.then(function(result) {
  console.log(result);
});

async-retry is built for async/await, offering the cleanest syntax for modern apps.

const retry = require('async-retry');

try {
  const result = await retry(async (bail, number) => {
    const res = await fetchData();
    if (res.status === 500) throw new Error('Server Error');
    return res;
  }, { retries: 3 });
  console.log(result);
} catch (err) {
  console.error(err);
}

exponential-backoff typically provides the delay logic, often used within a custom async loop.

import { backoff } from 'exponential-backoff';

await backoff(async () => {
  const res = await fetchData();
  if (!res.ok) throw new Error('Failed');
  return res;
}, {
  maxNumberOfRetries: 3,
});

βš™οΈ Configuration & Backoff Strategy

Controlling how long to wait between attempts is critical to avoid overwhelming services. Each package exposes these settings differently.

retry uses a timeouts array or factors to calculate delays.

const operation = retry.operation({
  retries: 3,
  factor: 2,
  minTimeout: 1000,
  maxTimeout: 5000
});

backoff lets you choose the strategy type (fibonacci, exponential) explicitly.

const exponentialBackoff = backoff.exponential({
  initialDelay: 1000,
  maxDelay: 5000
});

promise-retry passes options directly to the underlying retry logic.

promiseRetry(function(retry) {
  return fetchData();
}, {
  retries: 3,
  factor: 2,
  minTimeout: 1000
});

async-retry simplifies options to the most common needs.

await retry(async () => {
  return fetchData();
}, {
  retries: 3,
  factor: 2,
  minTimeout: 1000,
  randomize: true // Adds jitter
});

exponential-backoff focuses heavily on the delay math, often including jitter by default.

await backoff(async () => {
  return fetchData();
}, {
  maxNumberOfRetries: 3,
  startingDelay: 1000,
  timeMultiple: 2
});

πŸ›‘ Error Handling & Abort Control

Knowing when to stop retrying is as important as retrying itself. Some packages let you bail out early.

retry requires you to check operation.retry(err) manually to decide continuation.

if (err.code === 'ENOTFOUND') {
  operation.fail(err); // Stop immediately
  return;
}
if (operation.retry(err)) return;

backoff emits events you listen to, allowing logic inside the event handler.

fibBackoff.on('fail', function(err) {
  console.log('Stopped due to error', err);
});

promise-retry allows throwing specific errors to signal a hard failure.

.catch(function(err) {
  if (err.code === 'CRITICAL') {
    retry.cancel(); // Stop retries
  }
  throw err;
});

async-retry provides a bail function to stop retries immediately on non-retryable errors.

await retry(async (bail) => {
  const res = await fetchData();
  if (res.status === 404) {
    bail(new Error('Not Found')); // Stops retries
    return;
  }
  throw new Error('Retry me');
});

exponential-backoff usually relies on the promise rejection to stop, but some versions support abort signals.

const controller = new AbortController();

await backoff(async () => {
  if (someCondition) controller.abort();
  return fetchData();
}, {
  signal: controller.signal
});

πŸ“Š Summary Table

Featureretrybackoffpromise-retryasync-retryexponential-backoff
StyleCallbackCallbackPromiseAsync/AwaitAsync/Await
AgeLegacyLegacyMatureModernModern
Bail OutManual CheckEvent ListenerCancel Methodbail() FunctionAbort Signal
JitterrandomizeCustomInheritedrandomizeBuilt-in
Best ForLegacy NodeCustom Strategiesnpm ToolingFrontend/Next.jsCustom Loops

πŸ’‘ The Big Picture

retry and backoff are the foundations. They are battle-tested but show their age with callback patterns. Use them only if you are maintaining older systems or need specific low-level control over the backoff algorithm itself.

promise-retry is the bridge. It is excellent for tooling and CLI apps where promises are needed but the retry logic is preferred. It is less common in frontend UI code.

async-retry is the frontend favorite. Its API is designed for modern JavaScript. The bail function is particularly useful for handling 4xx errors that should not be retried. It is the safest choice for React, Vue, or Next.js projects.

exponential-backoff is a specialist. Use it if you are writing your own retry logic and just need a robust delay calculator, or if you need specific jitter configurations not found in the others.

Final Thought: For most new frontend development, async-retry offers the best balance of simplicity and power. It removes the boilerplate of older libraries while providing the control needed for production-grade resilience.

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

  • async-retry:

    Choose async-retry for modern frontend or Node.js projects using async/await. It is lightweight, promise-native, and maintained by Vercel, making it a safe bet for React and Next.js ecosystems where concise syntax is valued.

  • backoff:

    Choose backoff if you need fine-grained control over the backoff strategy itself, separate from the retry logic. It is best suited for older Node.js services that rely on callback patterns and require custom exponential or fibonacci strategies.

  • exponential-backoff:

    Choose exponential-backoff when you need a dedicated implementation of exponential backoff with jitter support without the overhead of a full retry wrapper. It is ideal for scenarios where you are building your own retry loop but need a reliable delay calculator.

  • promise-retry:

    Choose promise-retry if you are working in an environment that already uses the retry package but needs promise support. It is commonly found in npm internals and tooling, making it a solid choice for build scripts or CLI tools.

  • retry:

    Choose retry only for maintaining legacy systems that rely on callback-based flows. It is the foundational package for many others but lacks native promise support, making it less suitable for new frontend development.

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