Retry Strategies | Blue Frog Docs

Retry Strategies

Implement robust retry logic for failed network requests

Retry Strategies

What This Means

Network requests can fail for transient reasons (temporary outages, network blips). Without retry logic:

  • Single failures cause permanent errors
  • Users see error states unnecessarily
  • Data synchronization fails silently
  • Poor reliability on mobile networks

Retry Strategy Types

Strategy Best For
Immediate retry Quick transient failures
Exponential backoff Rate limiting, server overload
Linear backoff General transient errors
Jitter Distributed systems, avoiding thundering herd

General Fixes

Basic Retry with Exponential Backoff

async function fetchWithRetry(url, options = {}, maxRetries = 3) {
  let lastError;

  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      const response = await fetch(url, options);

      // Retry on server errors
      if (response.status >= 500) {
        throw new Error(`Server error: ${response.status}`);
      }

      return response;
    } catch (error) {
      lastError = error;

      if (attempt < maxRetries) {
        // Exponential backoff: 1s, 2s, 4s...
        const delay = Math.pow(2, attempt) * 1000;
        console.log(`Retry ${attempt + 1}/${maxRetries} after ${delay}ms`);
        await sleep(delay);
      }
    }
  }

  throw lastError;
}

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

Exponential Backoff with Jitter

async function fetchWithJitteredBackoff(url, options = {}, config = {}) {
  const {
    maxRetries = 3,
    baseDelay = 1000,
    maxDelay = 30000,
    jitterFactor = 0.5
  } = config;

  let lastError;

  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      const response = await fetch(url, options);

      if (!response.ok && response.status >= 500) {
        throw new Error(`Server error: ${response.status}`);
      }

      return response;
    } catch (error) {
      lastError = error;

      if (attempt < maxRetries) {
        // Calculate delay with exponential backoff
        let delay = Math.min(baseDelay * Math.pow(2, attempt), maxDelay);

        // Add jitter to prevent thundering herd
        const jitter = delay * jitterFactor * Math.random();
        delay = delay + jitter;

        console.log(`Retry ${attempt + 1}/${maxRetries} after ${Math.round(delay)}ms`);
        await sleep(delay);
      }
    }
  }

  throw new Error(`Failed after ${maxRetries} retries: ${lastError.message}`);
}

Retry Class with Configuration

class RetryableRequest {
  constructor(config = {}) {
    this.maxRetries = config.maxRetries ?? 3;
    this.baseDelay = config.baseDelay ?? 1000;
    this.maxDelay = config.maxDelay ?? 30000;
    this.retryableStatuses = config.retryableStatuses ?? [408, 429, 500, 502, 503, 504];
    this.retryableErrors = config.retryableErrors ?? ['ECONNRESET', 'ETIMEDOUT', 'ENOTFOUND'];
  }

  shouldRetry(error, response, attempt) {
    if (attempt >= this.maxRetries) return false;

    // Check response status
    if (response && this.retryableStatuses.includes(response.status)) {
      return true;
    }

    // Check error types
    if (error) {
      if (error.name === 'AbortError') return false; // Don't retry aborts
      if (error.name === 'TypeError') return true; // Network error
      if (this.retryableErrors.includes(error.code)) return true;
    }

    return false;
  }

  getDelay(attempt, response) {
    // Check for Retry-After header
    if (response?.headers) {
      const retryAfter = response.headers.get('Retry-After');
      if (retryAfter) {
        const seconds = parseInt(retryAfter, 10);
        if (!isNaN(seconds)) {
          return seconds * 1000;
        }
      }
    }

    // Exponential backoff with jitter
    const exponentialDelay = this.baseDelay * Math.pow(2, attempt);
    const jitter = exponentialDelay * 0.5 * Math.random();
    return Math.min(exponentialDelay + jitter, this.maxDelay);
  }

  async fetch(url, options = {}) {
    let lastError;
    let lastResponse;

    for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
      try {
        const response = await fetch(url, options);
        lastResponse = response;

        if (response.ok) {
          return response;
        }

        if (this.shouldRetry(null, response, attempt)) {
          const delay = this.getDelay(attempt, response);
          console.log(`Retry ${attempt + 1}/${this.maxRetries} after ${delay}ms (status: ${response.status})`);
          await this.sleep(delay);
          continue;
        }

        return response; // Non-retryable error status
      } catch (error) {
        lastError = error;

        if (this.shouldRetry(error, null, attempt)) {
          const delay = this.getDelay(attempt, null);
          console.log(`Retry ${attempt + 1}/${this.maxRetries} after ${delay}ms (error: ${error.message})`);
          await this.sleep(delay);
          continue;
        }

        throw error;
      }
    }

    throw lastError || new Error(`Request failed with status ${lastResponse?.status}`);
  }

  sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
}

// Usage
const client = new RetryableRequest({
  maxRetries: 5,
  baseDelay: 500
});

const response = await client.fetch('/api/data');

React Query Retry Configuration

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      retry: 3,
      retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
      // Custom retry logic
      retry: (failureCount, error) => {
        // Don't retry on 4xx errors
        if (error.response?.status >= 400 && error.response?.status < 500) {
          return false;
        }
        return failureCount < 3;
      }
    },
    mutations: {
      retry: 1, // Fewer retries for mutations
      retryDelay: 1000
    }
  }
});

SWR Retry Configuration

import useSWR from 'swr';

const { data, error } = useSWR('/api/data', fetcher, {
  errorRetryCount: 3,
  errorRetryInterval: 5000,
  onErrorRetry: (error, key, config, revalidate, { retryCount }) => {
    // Don't retry on 404
    if (error.status === 404) return;

    // Don't retry on 401/403
    if (error.status === 401 || error.status === 403) return;

    // Only retry up to 3 times
    if (retryCount >= 3) return;

    // Retry after 5 seconds
    setTimeout(() => revalidate({ retryCount }), 5000);
  }
});

Axios Retry Interceptor

import axios from 'axios';

const api = axios.create({
  baseURL: 'https://api.example.com'
});

api.interceptors.response.use(null, async (error) => {
  const config = error.config;

  // Initialize retry count
  config.__retryCount = config.__retryCount || 0;

  // Check if we should retry
  const shouldRetry = (
    config.__retryCount < 3 &&
    (!error.response || error.response.status >= 500)
  );

  if (!shouldRetry) {
    return Promise.reject(error);
  }

  config.__retryCount += 1;

  // Calculate delay
  const delay = Math.pow(2, config.__retryCount) * 1000;

  await new Promise(resolve => setTimeout(resolve, delay));

  return api(config);
});

Verification

  1. Test with simulated failures
  2. Verify exponential backoff timing
  3. Check max retry limits
  4. Confirm circuit breaker activates

Further Reading

// SYS.FOOTER