retry
The retry utility automatically re-executes an asynchronous function if it fails. It features customizable retry attempts, configurable delays, exponential backoff, and full support for AbortSignal to cancel pending retries.
Implementation
View Source Code
ts
import { Logit } from '@vielzeug/logit';
import { sleep } from './sleep';
/**
* Retries an asynchronous function a specified number of times with delay and optional exponential backoff.
*
* @example
* ```ts
* retry(() => fetchData(), { times: 3, delay: 1000, backoff: 2, signal: abortSignal })
* .then(result => console.log(result))
* .catch(error => console.error(error));
* ```
*
* @param fn - The asynchronous function to retry.
* @param options - (optional) Options for retrying the function.
* @param [options.times=3] - The number of retry attempts.
* @param [options.delay=250] - The delay in milliseconds between retries.
* @param [options.backoff=1] - Exponential backoff factor (default: 1 → no backoff).
* @param [options.signal] - `AbortSignal` to allow canceling retries.
*
* @returns The result of the asynchronous function.
*/
export async function retry<T>(
fn: () => Promise<T>,
{
times = 3,
delay = 250,
backoff = 1,
signal,
}: {
times?: number;
delay?: number;
backoff?: number | ((attempt: number, delay: number) => number);
signal?: AbortSignal;
} = {},
): Promise<T> {
let currentDelay = delay;
for (let attempt = 1; attempt <= times; attempt++) {
if (signal?.aborted) {
Logit.warn(`retry() -> Aborted after ${attempt - 1} attempts`);
throw new Error('Retry aborted');
}
try {
return await fn();
} catch (err) {
if (attempt === times) throw err;
Logit.warn(`retry() -> ${err}, attempt ${attempt}/${times}, retrying in ${currentDelay}ms`);
if (currentDelay > 0) await sleep(currentDelay);
currentDelay = typeof backoff === 'function' ? backoff(attempt, currentDelay) : currentDelay * backoff;
}
}
throw new Error('Retry failed unexpectedly');
}Features
- Isomorphic: Works in both Browser and Node.js.
- Exponential Backoff: Gradually increase delay between attempts to reduce system load.
- Abortable: Integration with
AbortSignalfor clean cancellation. - Type-safe: Properly infers the return type of the retried function.
API
ts
function retry<T>(
fn: () => Promise<T>,
options?: {
times?: number;
delay?: number;
backoff?: number | ((attempt: number, delay: number) => number);
signal?: AbortSignal;
},
): Promise<T>;Parameters
fn: The asynchronous function to execute.options: Optional configuration:times: Total number of attempts (defaults to3).delay: Initial wait time in milliseconds between retries (defaults to250).backoff: Multiplier for the delay after each failure (defaults to1, meaning no backoff). Can also be a function(attempt: number, delay: number) => numberfor custom backoff strategies.signal: AnAbortSignalto cancel the retry loop.
Returns
- A Promise that resolves with the value from
fn. - Rejects with the last error if all attempts fail, or an
AbortErrorif cancelled.
Examples
Basic Network Retry
ts
import { retry } from '@vielzeug/toolkit';
const data = await retry(
async () => {
const response = await fetch('/api/stats');
if (!response.ok) throw new Error('Failed');
return response.json();
},
{ times: 5, delay: 1000 },
);With Exponential Backoff
ts
import { retry } from '@vielzeug/toolkit';
// Delay sequence: 500ms, 1000ms, 2000ms...
await retry(heavyTask, {
times: 3,
delay: 500,
backoff: 2,
});With Custom Backoff Function
ts
import { retry } from '@vielzeug/toolkit';
// Custom backoff strategy
await retry(apiCall, {
times: 5,
delay: 100,
backoff: (attempt, currentDelay) => {
// Fibonacci-like backoff
return currentDelay + attempt * 100;
},
});Implementation Notes
- Performance-optimized retry loop using
await sleep(). - If the
signalis aborted, any pending delay is cleared immediately. - Throws
TypeErroriffnis not a function.