Yevhen Klymentiev
dark
light
console
darkness
y.klymentiev@gmail.com
Reusable Snippets|Practical utility code for everyday use — custom-built and ready to share

retryAsyncWithBackoff

Retries an async function with exponential backoff

TypeScript
Copied!
1/**
2 * Retries an async function with exponential backoff.
3 *
4 * @param fn - The async function to retry.
5 * @param retries - Max number of retries (default: 3).
6 * @param initialDelay - Delay in ms before first retry (default: 100).
7 * @param backoffFactor - Factor to multiply delay (default: 2).
8 * @returns The resolved value if successful, otherwise throws after final attempt.
9 */
10export async function retryAsyncWithBackoff<T>(
11  fn: () => Promise<T>,
12  retries: number = 3,
13  initialDelay: number = 100,
14  backoffFactor: number = 2
15): Promise<T> {
16  let attempt = 0;
17  let delay = initialDelay;
18
19  while (true) {
20    try {
21      return await fn();
22    } catch (error) {
23      if (attempt >= retries) {
24        throw error;
25      }
26      await new Promise(res => setTimeout(res, delay));
27      delay *= backoffFactor;
28      attempt++;
29    }
30  }
31}
  • Exponential Backoff Strategy

    Increases delay between retries exponentially, helping to avoid overwhelming services during temporary failures.

  • Customizable Parameters

    Allows fine-tuning of retry count, initial delay, and backoff factor to suit different use cases and failure profiles.

  • Efficient and Lightweight

    Minimal implementation without external dependencies, ideal for use in performance-sensitive environments.

  • Robust Failure Handling

    Retries only on actual failures and throws the last error if all attempts fail, preserving error context.

  • Promise-Based Design

    Naturally integrates with async/await patterns and error boundaries in modern codebases.

Tests | Examples

TypeScript
Copied!
1test('resolves on first try', async () => {
2  const fn = jest.fn().mockResolvedValue('ok');
3  const result = await retryAsyncWithBackoff(fn);
4  expect(result).toBe('ok');
5  expect(fn).toHaveBeenCalledTimes(1);
6});
7
8test('resolves after retries', async () => {
9  let count = 0;
10  const fn = jest.fn().mockImplementation(() => {
11    if (count++ < 2) return Promise.reject('fail');
12    return Promise.resolve('done');
13  });
14
15  const result = await retryAsyncWithBackoff(fn, 3, 10);
16  expect(result).toBe('done');
17  expect(fn).toHaveBeenCalledTimes(3);
18});
19
20test('throws after max retries', async () => {
21  const fn = jest.fn().mockRejectedValue(new Error('fail'));
22  await expect(
23    retryAsyncWithBackoff(fn, 2, 10)
24  ).rejects.toThrow('fail');
25  expect(fn).toHaveBeenCalledTimes(3); // initial + 2 retries
26});

Common Use Cases

  • Resilient API Requests

    Retry failing network calls (e.g., REST, GraphQL, WebSockets) with increasing delays to handle rate limits or flakiness.

  • Background Job Execution

    Stabilize async tasks like sending emails, uploading files, or processing messages that may occasionally fail.

  • Startup and Initialization Logic

    Retry bootstrapping code (e.g., service discovery, DB connection) that must succeed eventually.

  • Polling and Watchers

    Gracefully handle retryable fetch or sync cycles in polling loops or data watchers.

  • Third-Party Integration

    Add resilience around external APIs, payment gateways, or infrastructure services prone to temporary instability.

Codebase: Utilities -> Functions -> retryAsyncWithBackoff | Yevhen Klymentiev