Chrome Extension Retry Patterns — Best Practices

6 min read

Retry Patterns

Overview

Retry patterns are essential for handling transient failures in network requests and API calls. Chrome extensions often depend on external services, making reliable retry logic critical for a good user experience. The goal is to balance reliability with responsiveness—retrying failed operations enough to succeed, but not so much that users wait unnecessarily or overwhelm struggling services.

Simple Retry

The most basic approach is to retry a failed operation N times with a fixed delay between attempts:

async function fetchWithRetry(url, options, maxRetries = 3, delay = 1000) {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      const response = await fetch(url, options);
      if (!response.ok) throw new Error(`HTTP ${response.status}`);
      return await response.json();
    } catch (error) {
      if (attempt === maxRetries) throw error;
      await new Promise(r => setTimeout(r, delay));
    }
  }
}

Simple retries work best for idempotent operations—requests that produce the same result regardless of how many times they’re executed. Limit retries to 3-5 maximum to avoid frustrating users with long waits.

Exponential Backoff

Exponential backoff dramatically improves reliability by doubling the delay after each failed attempt:

function calculateBackoff(attempt, baseDelay = 1000, maxDelay = 60000) {
  const exponentialDelay = baseDelay * Math.pow(2, attempt);
  const jitter = Math.random() * baseDelay; // Prevent thundering herd
  return Math.min(exponentialDelay + jitter, maxDelay);
}

async function fetchWithBackoff(url, options, maxRetries = 5) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      return await fetch(url, options);
    } catch (error) {
      if (attempt === maxRetries - 1) throw error;
      await new Promise(r => setTimeout(r, calculateBackoff(attempt)));
    }
  }
}

This pattern produces delays like 1s, 2s, 4s, 8s, 16s—giving services time to recover. The jitter (random component) prevents all your users from retrying at exactly the same moment if there’s a widespread outage.

Retry with Circuit Breaker

Circuit breakers prevent your extension from hammering a broken service:

class CircuitBreaker {
  constructor(threshold = 5, timeout = 60000) {
    this.failures = 0;
    this.threshold = threshold;
    this.timeout = timeout;
    this.state = 'closed'; // closed, open, half-open
    this.nextAttempt = 0;
  }

  async execute(fn) {
    if (this.state === 'open' && Date.now() < this.nextAttempt) {
      throw new Error('Circuit breaker open');
    }

    try {
      const result = await fn();
      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure();
      throw error;
    }
  }

  onSuccess() {
    this.failures = 0;
    this.state = 'closed';
  }

  onFailure() {
    this.failures++;
    if (this.failures >= this.threshold) {
      this.state = 'open';
      this.nextAttempt = Date.now() + this.timeout;
    }
  }
}

States:

SW-Aware Retry

Service workers in extensions can be terminated between retries. Persist retry state:

// Store retry state in chrome.storage.session
async function saveRetryState(key, state) {
  await chrome.storage.session.set({ [`retry_${key}`]: state });
}

async function getRetryState(key) {
  const result = await chrome.storage.session.get(`retry_${key}`);
  return result[`retry_${key}`];
}

// Use chrome.alarms for delayed retries that survive SW termination
async function scheduleRetry(attempt, delayMs) {
  await chrome.alarms.create(`retry_${attempt}`, { delayInMinutes: delayMs / 60000 });
}

// Handle alarm in service worker
chrome.alarms.onAlarm.addListener((alarm) => {
  if (alarm.name.startsWith('retry_')) {
    const attempt = parseInt(alarm.name.split('_')[1]);
    // Resume retry logic
  }
});

This ensures retries continue even if the service worker is unloaded between attempts.

What to Retry

Retry these conditions:

function shouldRetry(error, response) {
  if (error) return true; // Network error
  if (!response) return false;
  
  const status = response.status;
  return status === 429 || status >= 500;
}

function getRetryAfter(response) {
  const retryAfter = response.headers.get('Retry-After');
  return retryAfter ? parseInt(retryAfter) * 1000 : null;
}

What NOT to Retry

Never retry these operations:

See also:

Part of the Chrome Extension Guide by theluckystrike. Built at zovo.one.