$ snippet fetch-retry-backoff

Retry fetch with exponential backoff

Retry failed requests with growing delays and jitter, only for errors that are worth retrying.

JavaScript
/**
 * fetch with retries. Retries on network errors, 408, 429 and 5xx.
 * Honors Retry-After. Delays: base * 2^attempt (+ jitter), capped at maxDelay.
 */
async function fetchWithRetry(url, options = {}, {
  retries = 3, base = 300, maxDelay = 8000, signal = options.signal,
} = {}) {
  let lastError;
  for (let attempt = 0; attempt <= retries; attempt++) {
    try {
      const res = await fetch(url, options);
      if (res.ok || !shouldRetry(res.status)) return res;
      lastError = new Error(`HTTP ${res.status}`);
      if (attempt === retries) return res;               // give caller the final response
      await sleep(retryDelay(res, attempt, base, maxDelay), signal);
    } catch (err) {
      if (err.name === "AbortError") throw err;           // caller cancelled: stop
      lastError = err;
      if (attempt === retries) throw err;
      await sleep(retryDelay(null, attempt, base, maxDelay), signal);
    }
  }
  throw lastError;
}

const shouldRetry = (status) => status === 408 || status === 429 || status >= 500;

function retryDelay(res, attempt, base, maxDelay) {
  const header = res?.headers.get("Retry-After");
  if (header) {
    const secs = Number(header);
    if (!Number.isNaN(secs)) return Math.min(secs * 1000, maxDelay);
    const at = Date.parse(header);
    if (!Number.isNaN(at)) return Math.max(0, Math.min(at - Date.now(), maxDelay));
  }
  const exp = base * 2 ** attempt;
  return Math.min(maxDelay, exp / 2 + Math.random() * exp / 2);   // jitter: 50–100% of exp
}

function sleep(ms, signal) {
  return new Promise((resolve, reject) => {
    const t = setTimeout(resolve, ms);
    signal?.addEventListener("abort", () => { clearTimeout(t); reject(signal.reason ?? new DOMException("Aborted", "AbortError")); }, { once: true });
  });
}

How it works

Retrying everything is a bug: a 404 or 400 will never succeed, and retrying a non-idempotent POST can create duplicate orders. This version retries only transient failures, backs off exponentially so a struggling server isn't hammered, adds jitter so many clients don't retry in lockstep, and respects Retry-After when the server tells you how long to wait.

Gotchas

Try it
Result
Ad slot