typescriptintermediate

Retry with Exponential Backoff

Retry failed async operations with exponential backoff, jitter, and configurable retry conditions.

typescript
interface RetryOptions {
  maxAttempts: number;
  baseDelay: number;
  maxDelay: number;
  jitter: boolean;
  retryIf?: (error: Error) => boolean;
  onRetry?: (error: Error, attempt: number) => void;
}

const defaultOptions: RetryOptions = {
  maxAttempts: 3,
  baseDelay: 1000,
  maxDelay: 30000,
  jitter: true,
};

async function retry<T>(
  fn: (attempt: number) => Promise<T>,
  opts?: Partial<RetryOptions>,
): Promise<T> {
  const options = { ...defaultOptions, ...opts };
  let lastError: Error;

  for (let attempt = 1; attempt <= options.maxAttempts; attempt++) {
    try {
      return await fn(attempt);
    } catch (err) {
      lastError = err instanceof Error ? err : new Error(String(err));

      if (attempt === options.maxAttempts) break;
      if (options.retryIf && !options.retryIf(lastError)) break;

      let delay = Math.min(
        options.baseDelay * Math.pow(2, attempt - 1),
        options.maxDelay,
      );
      if (options.jitter) delay *= 0.5 + Math.random();

      options.onRetry?.(lastError, attempt);
      await new Promise(r => setTimeout(r, delay));
    }
  }

  throw lastError!;
}

// Circuit breaker
class CircuitBreaker {
  private failures = 0;
  private lastFailure = 0;
  private state: 'closed' | 'open' | 'half-open' = 'closed';

  constructor(
    private threshold: number = 5,
    private resetTimeout: number = 60000,
  ) {}

  async execute<T>(fn: () => Promise<T>): Promise<T> {
    if (this.state === 'open') {
      if (Date.now() - this.lastFailure > this.resetTimeout) {
        this.state = 'half-open';
      } else {
        throw new Error('Circuit breaker is OPEN');
      }
    }

    try {
      const result = await fn();
      this.failures = 0;
      this.state = 'closed';
      return result;
    } catch (err) {
      this.failures++;
      this.lastFailure = Date.now();
      if (this.failures >= this.threshold) this.state = 'open';
      throw err;
    }
  }
}

// Usage
async function main() {
  let callCount = 0;
  const result = await retry(
    async (attempt) => {
      callCount++;
      if (callCount < 3) throw new Error(`Fail #${callCount}`);
      return 'Success!';
    },
    {
      maxAttempts: 5,
      baseDelay: 100,
      onRetry: (err, attempt) => console.log(`Retry ${attempt}: ${err.message}`),
    },
  );
  console.log(result);
}

main();

Use Cases

  • API call resilience
  • Database reconnection
  • Transient error recovery

Tags

Related Snippets

Similar patterns you can reuse in the same workflow.