Maintained, drop-in replacement for async-retry.
Calls an async (or sync) function until it succeeds, waiting longer after each failure. Same API and same delays as async-retry, without the retry dependency.
- Same API as
async-retry. Its test suite is ported and runs against both packages - Fixes long-standing upstream issues (see below)
- TypeScript types included
- ESM and CommonJS
- Zero dependencies
- 100% test coverage
npm i @tendjs/async-retry- import retry from 'async-retry'
+ import retry from '@tendjs/async-retry'If you used @types/async-retry, remove it. Types ship with the package, and retry.Options and retry.RetryFunction still work.
To switch without touching your imports, install it under the old name:
npm i async-retry@npm:@tendjs/async-retry@^1If a dependency of yours uses async-retry, swap it there too:
With npm, $async-retry reuses the version from your own dependencies (npm rejects an override that differs from it). If async-retry is not in your dependencies, use "npm:@tendjs/async-retry@^1" instead.
import retry from '@tendjs/async-retry'
const data = await retry(
async (bail) => {
// if anything throws, we retry
const res = await fetch('https://example.com')
if (res.status === 403) {
// don't retry on 403
bail(new Error('Unauthorized'))
return
}
return res.text()
},
{ retries: 5 },
)require('@tendjs/async-retry') returns the function, like async-retry did. import { retry } from '@tendjs/async-retry' works as well.
function retry<T>(
fn: (bail: (error?: unknown) => void, attempt: number) => T | Promise<T>,
options?: Options,
): Promise<T>fn can be async or not. It gets two arguments:
bail(error?): stops retrying and rejects witherror, or withError('Aborted')if you pass nothing. After that, neitherfnnoronRetryis called again.attempt: the attempt number. The first call is1.
Throwing an error that has bail: true set also stops retrying.
When retries run out, the promise rejects with the error whose message came up most often (ties go to the later one).
| Option | Default | |
|---|---|---|
retries |
10 |
How many times to retry after the first attempt. Infinity retries until success. |
factor |
2 |
Exponential factor between delays. |
minTimeout |
1000 |
Milliseconds before the first retry. |
maxTimeout |
Infinity |
Maximum milliseconds between two retries. |
randomize |
true |
Multiply each delay by a random factor between 1 and 2. |
forever |
false |
Keep retrying with the longest delay once retries run out. |
maxRetryTime |
Infinity |
Stop retrying this many milliseconds after the first attempt. |
unref |
false |
Don't keep the process alive while waiting for a retry. |
onRetry(error, attempt) |
Runs after an attempt fails and before the next one starts. If it returns a promise, the next attempt waits for it. If it throws or rejects, retrying stops with that error. | |
shouldRetry(error, attempt) |
Runs after an attempt fails. Return false (or a promise of false) to stop and reject with that error. |
Delays are computed like retry does it: minTimeout * factor ** n, times the random factor, capped at maxTimeout.
| Upstream issue | Fix | Test |
|---|---|---|
| #69 | bail() stops retrying for good. Upstream kept retrying if the function threw after calling bail(), and a retry that was already scheduled still ran. |
upstream #69: bail should prevent any further calls to the retrier function |
| #79 | An error thrown in onRetry (or a rejected promise) rejects the retry promise. Upstream lost the error and kept retrying. |
upstream #79: throw Error inside onRetry |
| #43 | An async onRetry is awaited before the next attempt. |
upstream #43: Can onRetry handle async functions? |
| #51 | New shouldRetry option. |
upstream #51: Add option to decide if retry should be attempted? |
| #36, #97 | Types ship with the package and include every option. | upstream #36: Types for Typescript, upstream #97: Option has no retries |
| #103 | import { retry } works from ESM and CommonJS. |
upstream #103: (0 , async_retry_1.retry) is not a function |
| #92 | The docs say onRetry runs before the next attempt, which is what it does. |
upstream #92: The documentation for onRetry is wrong |
Bugs I found while reading the code, without an upstream issue:
throw nullorthrow undefinedcrashed with aTypeErrorand, from an async function, left the promise pending forever. Other falsy values (0,'',false) stopped retrying. All of them are now retried like any other error.retries: Infinityhung the process:retrytried to build an infinite list of delays. Now it retries until success, with delays growing up tomaxTimeout.- Delays above 2^31-1 ms (about 24.8 days) fired after 1 ms, because that is what
setTimeoutdoes with them. They are capped instead. unref: truethrew aTypeErrorin browsers, wheresetTimeoutreturns a number that has nounref(). The option is now ignored there.
Apart from the fixes above:
- The options object you pass is not modified.
async-retrysetoptions.randomize = trueon it.
Any other behaviour difference is a bug. Please open an issue.
Based on async-retry by Guillermo Rauch and Leo Lamprecht (Vercel). The delay calculation follows retry by Tim Koschützki. See NOTICE.md. Project setup based on antfu/starter-ts.
MIT License © Adnan Labiadh