← Back to Home

How to Implement Retry Logic with Exponential Backoff

Updated January 14, 2026
retryexponential backofferror handlingresiliencetypescript

Implementing Retry Logic with Exponential Backoff

Retry logic with exponential backoff improves resilience for transient network failures.

Basic Retry Pattern

async function retryWithBackoff<T>(
  operation: () => Promise<T>,
  maxRetries: number = 3
): Promise<T> {
  let lastError: Error | null = null;

  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await operation();
    } catch (error) {
      lastError = error as Error;

      // Don't retry on last attempt
      if (attempt === maxRetries) {
        throw error;
      }

      // Check if error is retryable
      if (!isRetryableError(error)) {
        throw error;
      }

      // Exponential backoff
      const backoffMs = Math.pow(2, attempt) * 1000;
      console.log(`Attempt ${attempt} failed, retrying in ${backoffMs}ms...`);
      await sleep(backoffMs);
    }
  }

  throw lastError;
}

Sleep Utility

function sleep(ms: number): Promise<void> {
  return new Promise(resolve => setTimeout(resolve, ms));
}

Retryable Error Detection

function isRetryableError(error: any): boolean {
  // HTTP status codes that are retryable
  const retryableStatusCodes = [408, 429, 500, 502, 503, 504];

  if (error.code && retryableStatusCodes.includes(error.code)) {
    return true;
  }

  // Network errors
  const errorMessage = error.message?.toLowerCase() || '';
  const retryableMessages = [
    'timeout',
    'econnreset',
    'enotfound',
    'econnrefused',
    'socket hang up',
    'network',
  ];

  return retryableMessages.some(msg => errorMessage.includes(msg));
}

Backoff Strategies

Exponential Backoff

const backoffMs = Math.pow(2, attempt) * 1000;
// 1s, 2s, 4s, 8s, 16s...

Exponential Backoff with Jitter

function calculateBackoff(attempt: number, baseMs: number = 1000): number {
  const exponential = Math.pow(2, attempt) * baseMs;
  const jitter = Math.random() * baseMs;  // Add randomness
  return exponential + jitter;
}

Linear Backoff

const backoffMs = attempt * 2000;  // 2s, 4s, 6s...

Fixed Delay

const backoffMs = 3000;  // Always 3 seconds

With Max Delay Cap

function calculateBackoffWithCap(
  attempt: number,
  baseMs: number = 1000,
  maxMs: number = 30000
): number {
  const exponential = Math.pow(2, attempt) * baseMs;
  return Math.min(exponential, maxMs);
}

Complete Retry Class

class RetryManager {
  constructor(
    private maxRetries: number = 3,
    private baseDelayMs: number = 1000,
    private maxDelayMs: number = 30000
  ) {}

  async execute<T>(
    operation: () => Promise<T>,
    isRetryable: (error: any) => boolean = isRetryableError
  ): Promise<T> {
    let lastError: Error | null = null;

    for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
      try {
        return await operation();
      } catch (error) {
        lastError = error as Error;

        if (attempt === this.maxRetries || !isRetryable(error)) {
          throw error;
        }

        const delay = this.calculateDelay(attempt);
        console.log(`Retry ${attempt}/${this.maxRetries} in ${delay}ms`);
        await sleep(delay);
      }
    }

    throw lastError;
  }

  private calculateDelay(attempt: number): number {
    const exponential = Math.pow(2, attempt) * this.baseDelayMs;
    return Math.min(exponential, this.maxDelayMs);
  }
}

Usage Example

const retryManager = new RetryManager(3, 1000, 30000);

const result = await retryManager.execute(
  async () => {
    const response = await fetch('https://api.example.com/data');
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }
    return response.json();
  },
  (error) => error.status >= 500 || error.status === 429
);

For Specific Use Cases

API Calls

async function fetchWithRetry(url: string, maxRetries: number = 3): Promise<any> {
  return retryWithBackoff(async () => {
    const response = await fetch(url);
    if (!response.ok) {
      const error: any = new Error(`HTTP ${response.status}`);
      error.code = response.status;
      throw error;
    }
    return response.json();
  }, maxRetries);
}

Database Operations

async function queryWithRetry(query: string, maxRetries: number = 3): Promise<any> {
  return retryWithBackoff(async () => {
    return await database.execute(query);
  }, maxRetries);
}

File Operations

async function readFileWithRetry(filePath: string, maxRetries: number = 3): Promise<Buffer> {
  return retryWithBackoff(async () => {
    return await fs.promises.readFile(filePath);
  }, maxRetries);
}

Logging Retry Attempts

async function retryWithLogging<T>(
  operation: () => Promise<T>,
  maxRetries: number = 3
): Promise<T> {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      const result = await operation();
      if (attempt > 1) {
        console.log(`Operation succeeded on attempt ${attempt}`);
      }
      return result;
    } catch (error) {
      console.error(`Attempt ${attempt} failed:`, error.message);

      if (attempt === maxRetries) {
        console.error(`All ${maxRetries} attempts failed`);
        throw error;
      }
    }
  }
  throw new Error('Retry failed');
}