Skip to content
VersionSize

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 AbortSignal for 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 to 3).
    • delay: Initial wait time in milliseconds between retries (defaults to 250).
    • backoff: Multiplier for the delay after each failure (defaults to 1, meaning no backoff). Can also be a function (attempt: number, delay: number) => number for custom backoff strategies.
    • signal: An AbortSignal to 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 AbortError if 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 signal is aborted, any pending delay is cleared immediately.
  • Throws TypeError if fn is not a function.

See Also

  • predict: Wait for a condition to become true.
  • sleep: Pause execution for a specified duration.
  • debounce: Rate-limit function execution.