retry vs retry-request vs async-retry vs promise-retry vs retry-axios vs backoff
Strategic Retry Logic and Resilience Patterns in JavaScript
retryretry-requestasync-retrypromise-retryretry-axiosbackoffSimilar Packages:

Strategic Retry Logic and Resilience Patterns in JavaScript

This comparison evaluates six prominent npm packages designed to handle transient failures in JavaScript applications through retry mechanisms. While they share the common goal of improving system resilience against network flakiness or temporary service unavailability, they differ significantly in their execution models (callbacks vs. promises), integration scope (generic utilities vs. HTTP client wrappers), and maintenance status. Some packages like retry and backoff represent older, callback-era patterns, while others like async-retry and promise-retry modernize these concepts for async/await workflows. Specialized wrappers like retry-axios and retry-request offer drop-in solutions for specific HTTP libraries but introduce tighter coupling. Understanding these distinctions is critical for architects deciding between building custom resilience logic versus adopting specialized, potentially fragile, third-party wrappers.

Npm Package Weekly Downloads Trend

3 Years

Github Stars Ranking

Stat Detail

Package
Downloads
Stars
Size
Issues
Publish
License
retry69,007,8091,256-275 years agoMIT
retry-request22,878,6273,20916.8 kB64110 days agoMIT
async-retry20,069,2921,918-305 years agoMIT
promise-retry15,784,227318-116 years agoMIT
retry-axios1,323,54751264.9 kB06 months agoApache-2.0
backoff0336-1111 years agoMIT

Strategic Retry Logic and Resilience Patterns in JavaScript

Building resilient frontend and Node.js applications means accepting that networks fail, APIs timeout, and services glitch. The difference between a crashing app and a seamless user experience often comes down to how you handle these transient errors. The JavaScript ecosystem offers several tools for this, ranging from low-level primitives to high-level HTTP wrappers. Let's dive into six specific packagesโ€”async-retry, backoff, promise-retry, retry, retry-axios, and retry-requestโ€”to understand when each belongs in your architecture.

๐Ÿ”„ Core Execution Models: Callbacks vs. Promises

The most fundamental split in this group is between older callback-based designs and modern promise-driven workflows. This dictates how the code reads and how it fits into your existing async/await structures.

retry is the grandfather of this list. It uses a callback pattern where you pass a function to attempt, and a separate callback to handle the result or error. It forces you into a nested structure that feels out of place in modern codebases.

// retry: Callback-based
const operation = new RetryOperation(retries);

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

backoff follows a similar legacy approach, focusing specifically on the timing strategy rather than the whole operation flow. You create a backoff instance and wire up events for when to try again.

// backoff: Event-driven callback
const backoff = require('backoff');
const fibBackoff = backoff.fibonacci();

fibBackoff.on('ready', function() {
  fetchData(function(err, result) {
    if (err) {
      fibBackoff.backoff(); // Trigger next attempt after delay
    } else {
      fibBackoff.reset();
      console.log(result);
    }
  });
});

fibBackoff.start();

promise-retry bridges the gap. It takes the logic of retry but wraps it so you can return promises. It still expects a function that returns a promise, but it handles the chaining internally.

// promise-retry: Promise wrapper
const promiseRetry = require('promise-retry');

promiseRetry(function(retry, number) {
  return fetchData()
    .catch(function(err) {
      if (number <= 3) {
        return retry(err); // Pass error to retry logic
      }
      throw err;
    });
}, { retries: 3 });

async-retry is the modern standard. It is built entirely around async/await. You pass an async function, and it handles the looping internally. The code looks synchronous and clean, which reduces cognitive load.

// async-retry: Native async/await
const retry = require('async-retry');

try {
  const result = await retry(
    async (bail) => {
      const res = await fetchData();
      if (res.status === 404) {
        bail(new Error('Not found')); // Stop retrying immediately
        return;
      }
      return res;
    },
    { retries: 3 }
  );
  console.log(result);
} catch (err) {
  console.error('Failed after all retries', err);
}

๐ŸŒ HTTP Specific Wrappers vs. Generic Utilities

A critical architectural decision is whether to use a generic tool that works for databases, file systems, and APIs, or a specialized tool tied to a specific HTTP library.

retry-axios is a specialized interceptor for Axios. It attaches directly to the Axios instance. This is convenient but creates a hard dependency on Axios. If you ever switch to fetch or ky, this logic becomes useless.

// retry-axios: Axios Interceptor
const axios = require('axios');
const setupRetry = require('retry-axios');

const client = axios.create();
setupRetry({ axios: client });

// Automatically retries failed requests on this instance
client.get('/api/data').then(res => console.log(res.data));

retry-request is similarly tied to the request library (and by extension, many Google Cloud Node.js clients). Since the request library itself is deprecated and unmaintained, relying on this wrapper introduces significant technical debt.

// retry-request: Tied to 'request' library
const request = require('retry-request');

request({ uri: 'https://api.example.com/data' }, function(err, res, body) {
  if (err) return console.error(err);
  console.log(body);
});

async-retry, promise-retry, retry, and backoff are generic. They don't care if you are fetching data, reading a file, or querying a database. This makes them more future-proof.

// async-retry: Generic usage (Database example)
await retry(async () => {
  return await db.query('SELECT * FROM users');
});

// async-retry: Generic usage (File system example)
await retry(async () => {
  return await fs.promises.readFile('config.json');
});

โš ๏ธ Maintenance Status and Deprecation Risks

In architectural decisions, the lifespan of a library is as important as its features. Several packages in this list carry heavy warnings.

retry-request depends on request, which has been officially deprecated since 2020. Using this in a new project is strongly discouraged. The ecosystem has moved to node-fetch, axios, or native fetch. If you see this in a codebase, plan to migrate away from it.

backoff and retry show signs of stagnation. While they still work, they lack the ergonomic improvements of modern JavaScript. They are "safe" in legacy apps but poor choices for greenfield development because they force callback hell.

async-retry is actively maintained and widely adopted in the modern Node.js ecosystem (including by Vercel and other major platforms). It receives updates to match current JavaScript standards.

retry-axios is useful but risky. Its life cycle is tied to Axios's internal interceptor API. If Axios makes a breaking change to how interceptors handle errors, this package could break unexpectedly.

๐ŸŽฏ Controlling the Retry Flow

How do you decide when to stop retrying? Different packages offer different levels of control.

async-retry gives you a bail function. This is powerful because it lets you distinguish between "try again" errors (like a 503 Service Unavailable) and "stop now" errors (like a 404 Not Found or a validation error).

// async-retry: Precise control with bail
await retry(async (bail) => {
  const response = await fetch('/api/resource');
  
  if (response.status === 404) {
    // Don't retry for 404s
    bail(new Error('Resource missing'));
    return;
  }
  
  if (!response.ok) {
    // Throw to trigger a retry
    throw new Error('Server error, retrying...');
  }
  
  return response.json();
});

promise-retry relies on you calling the retry function passed into your callback. It is slightly more verbose to implement conditional logic compared to bail.

// promise-retry: Manual retry trigger
await promiseRetry((retry, number) => {
  return fetchData().catch(err => {
    if (err.status === 404) {
      throw err; // Stops retrying
    }
    if (number < 3) {
      return retry(err); // Continues retrying
    }
    throw err;
  });
});

retry-axios handles this via configuration objects where you define which status codes should trigger a retry. It is less flexible for complex logic but sufficient for standard HTTP patterns.

// retry-axios: Config-based control
setupRetry({
  axios: client,
  retryDelay: 1000,
  retryAttempts: 3,
  shouldRetry: (error) => {
    // Custom logic to decide retry
    return error.response && error.response.status === 503;
  }
});

๐Ÿ“Š Summary of Trade-offs

Featureasync-retrypromise-retryretry / backoffretry-axiosretry-request
StyleAsync/AwaitPromisesCallbacksInterceptorCallback/Stream
ScopeGenericGenericGenericAxios OnlyRequest Only
MaintenanceActiveStableLegacy/StaleModerateDeprecated Risk
Controlbail() functionretry() callEvent/CallbackConfig ObjectConfig Object
Best ForModern AppsLegacy PromisesVery Old CodeAxios ProjectsGoogle Cloud Legacy

๐Ÿ’ก The Architect's Recommendation

For nearly all modern frontend and Node.js projects, async-retry is the superior choice. It is framework-agnostic, meaning you can use it with fetch, axios, graphql, or database clients without locking yourself into a specific HTTP library. Its support for the bail pattern provides the clarity needed to handle complex error scenarios without nesting callbacks.

Reserve retry-axios only for quick prototypes where you are 100% certain Axios will remain your HTTP client forever. In large-scale enterprise applications, the coupling it introduces is often not worth the minor convenience.

Finally, treat retry-request, retry, and backoff as legacy artifacts. If you encounter them during maintenance, schedule time to refactor them into async-retry or native async/await loops. Building new features on top of deprecated or callback-heavy foundations slows down development and increases the risk of future breakage.

Resilience is essential, but it shouldn't come at the cost of code clarity or future flexibility. Choose tools that grow with your stack, not ones that anchor you to the past.

How to Choose: retry vs retry-request vs async-retry vs promise-retry vs retry-axios vs backoff

  • retry:

    Do not use retry in new frontend or modern Node.js architectures. It is a foundational but outdated callback-based library. Use it only if you are maintaining legacy systems where changing the control flow from callbacks to promises is too risky or costly.

  • retry-request:

    Choose retry-request exclusively if your infrastructure relies heavily on the request library (which is itself deprecated) or Google Cloud libraries that depend on it. For any other HTTP client, this package is obsolete and should be avoided.

  • async-retry:

    Choose async-retry for modern, promise-based applications where you need a lightweight, generic utility to wrap any async operation (not just HTTP). It is the most robust choice for new projects requiring custom retry logic with clean async/await syntax and active maintenance.

  • promise-retry:

    Select promise-retry if your project already uses the retry package logic but requires a Promise interface, or if you need specific compatibility with npm-style retry behaviors. It is a solid middle-ground for generic promise retries but lacks the modern DX of async-retry.

  • retry-axios:

    Use retry-axios only if you are strictly bound to the Axios ecosystem and need a quick, zero-config way to add retries to existing interceptors. Be aware that this adds a dependency on Axios internals, which may break if Axios updates its interceptor API significantly.

  • backoff:

    Avoid choosing backoff for new projects as it relies on legacy callback patterns and appears largely unmaintained. Only consider it if you are refactoring a very old codebase that already depends on its specific exponential backoff strategy and cannot be easily migrated to promise-based alternatives.

README for retry

Build Status codecov

retry

Abstraction for exponential and custom retry strategies for failed operations.

Installation

npm install retry

Current Status

This module has been tested and is ready to be used.

Tutorial

The example below will retry a potentially failing dns.resolve operation 10 times using an exponential backoff strategy. With the default settings, this means the last attempt is made after 17 minutes and 3 seconds.

var dns = require('dns');
var retry = require('retry');

function faultTolerantResolve(address, cb) {
  var operation = retry.operation();

  operation.attempt(function(currentAttempt) {
    dns.resolve(address, function(err, addresses) {
      if (operation.retry(err)) {
        return;
      }

      cb(err ? operation.mainError() : null, addresses);
    });
  });
}

faultTolerantResolve('nodejs.org', function(err, addresses) {
  console.log(err, addresses);
});

Of course you can also configure the factors that go into the exponential backoff. See the API documentation below for all available settings. currentAttempt is an int representing the number of attempts so far.

var operation = retry.operation({
  retries: 5,
  factor: 3,
  minTimeout: 1 * 1000,
  maxTimeout: 60 * 1000,
  randomize: true,
});

API

retry.operation([options])

Creates a new RetryOperation object. options is the same as retry.timeouts()'s options, with three additions:

  • forever: Whether to retry forever, defaults to false.
  • unref: Whether to unref the setTimeout's, defaults to false.
  • maxRetryTime: The maximum time (in milliseconds) that the retried operation is allowed to run. Default is Infinity.

retry.timeouts([options])

Returns an array of timeouts. All time options and return values are in milliseconds. If options is an array, a copy of that array is returned.

options is a JS object that can contain any of the following keys:

  • retries: The maximum amount of times to retry the operation. Default is 10. Seting this to 1 means do it once, then retry it once.
  • 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 false.

The formula used to calculate the individual timeouts is:

Math.min(random * minTimeout * Math.pow(factor, attempt), maxTimeout)

Have a look at this article for a better explanation of approach.

If you want to tune your factor / times settings to attempt the last retry after a certain amount of time, you can use wolfram alpha. For example in order to tune for 10 attempts in 5 minutes, you can use this equation:

screenshot

Explaining the various values from left to right:

  • k = 0 ... 9: The retries value (10)
  • 1000: The minTimeout value in ms (1000)
  • x^k: No need to change this, x will be your resulting factor
  • 5 * 60 * 1000: The desired total amount of time for retrying in ms (5 minutes)

To make this a little easier for you, use wolfram alpha to do the calculations:

http://www.wolframalpha.com/input/?i=Sum%5B1000*x^k%2C+{k%2C+0%2C+9}%5D+%3D+5+*+60+*+1000

retry.createTimeout(attempt, opts)

Returns a new timeout (integer in milliseconds) based on the given parameters.

attempt is an integer representing for which retry the timeout should be calculated. If your retry operation was executed 4 times you had one attempt and 3 retries. If you then want to calculate a new timeout, you should set attempt to 4 (attempts are zero-indexed).

opts can include factor, minTimeout, randomize (boolean) and maxTimeout. They are documented above.

retry.createTimeout() is used internally by retry.timeouts() and is public for you to be able to create your own timeouts for reinserting an item, see issue #13.

retry.wrap(obj, [options], [methodNames])

Wrap all functions of the obj with retry. Optionally you can pass operation options and an array of method names which need to be wrapped.

retry.wrap(obj)

retry.wrap(obj, ['method1', 'method2'])

retry.wrap(obj, {retries: 3})

retry.wrap(obj, {retries: 3}, ['method1', 'method2'])

The options object can take any options that the usual call to retry.operation can take.

new RetryOperation(timeouts, [options])

Creates a new RetryOperation where timeouts is an array where each value is a timeout given in milliseconds.

Available options:

  • forever: Whether to retry forever, defaults to false.
  • unref: Wether to unref the setTimeout's, defaults to false.

If forever is true, the following changes happen:

  • RetryOperation.errors() will only output an array of one item: the last error.
  • RetryOperation will repeatedly use the timeouts array. Once all of its timeouts have been used up, it restarts with the first timeout, then uses the second and so on.

retryOperation.errors()

Returns an array of all errors that have been passed to retryOperation.retry() so far. The returning array has the errors ordered chronologically based on when they were passed to retryOperation.retry(), which means the first passed error is at index zero and the last is at the last index.

retryOperation.mainError()

A reference to the error object that occured most frequently. Errors are compared using the error.message property.

If multiple error messages occured the same amount of time, the last error object with that message is returned.

If no errors occured so far, the value is null.

retryOperation.attempt(fn, timeoutOps)

Defines the function fn that is to be retried and executes it for the first time right away. The fn function can receive an optional currentAttempt callback that represents the number of attempts to execute fn so far.

Optionally defines timeoutOps which is an object having a property timeout in miliseconds and a property cb callback function. Whenever your retry operation takes longer than timeout to execute, the timeout callback function cb is called.

retryOperation.try(fn)

This is an alias for retryOperation.attempt(fn). This is deprecated. Please use retryOperation.attempt(fn) instead.

retryOperation.start(fn)

This is an alias for retryOperation.attempt(fn). This is deprecated. Please use retryOperation.attempt(fn) instead.

retryOperation.retry(error)

Returns false when no error value is given, or the maximum amount of retries has been reached.

Otherwise it returns true, and retries the operation after the timeout for the current attempt number.

retryOperation.stop()

Allows you to stop the operation being retried. Useful for aborting the operation on a fatal error etc.

retryOperation.reset()

Resets the internal state of the operation object, so that you can call attempt() again as if this was a new operation object.

retryOperation.attempts()

Returns an int representing the number of attempts it took to call fn before it was successful.

License

retry is licensed under the MIT license.

Changelog

0.10.0 Adding stop functionality, thanks to @maxnachlinger.

0.9.0 Adding unref functionality, thanks to @satazor.

0.8.0 Implementing retry.wrap.

0.7.0 Some bug fixes and made retry.createTimeout() public. Fixed issues #10, #12, and #13.

0.6.0 Introduced optional timeOps parameter for the attempt() function which is an object having a property timeout in milliseconds and a property cb callback function. Whenever your retry operation takes longer than timeout to execute, the timeout callback function cb is called.

0.5.0 Some minor refactoring.

0.4.0 Changed retryOperation.try() to retryOperation.attempt(). Deprecated the aliases start() and try() for it.

0.3.0 Added retryOperation.start() which is an alias for retryOperation.try().

0.2.0 Added attempts() function and parameter to retryOperation.try() representing the number of attempts it took to call fn().