Retry для API-запросов

Автоматическое повторение упавшего fetch-запроса с экспоненциальной задержкой — для работы с нестабильными API.

Задача

Сетевые запросы иногда падают из-за временных сбоев сервера (503, таймаут). Вместо показа ошибки пользователю нужно несколько раз повторить запрос с нарастающей паузой между попытками.

Решение

// utils/retry.js

async function fetchWithRetry(url, options = {}, retries = 3, backoff = 300) {
  for (let attempt = 0; attempt <= retries; attempt++) {
    try {
      const response = await fetch(url, options);

      // Повторяем только при серверных ошибках (5xx), но не клиентских (4xx)
      if (response.status >= 500 && attempt < retries) {
        await delay(backoff * 2 ** attempt); // 300, 600, 1200 мс
        continue;
      }

      return response;
    } catch (err) {
      // Сетевая ошибка (нет соединения)
      if (attempt === retries) throw err;
      await delay(backoff * 2 ** attempt);
    }
  }
}

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

export { fetchWithRetry };

Использование:

import { fetchWithRetry } from './utils/retry.js';

// До 3 повторных попыток, начальная задержка 300 мс
const response = await fetchWithRetry('/api/data', {}, 3, 300);
const data = await response.json();

TypeScript-версия с типами и jitter:

interface RetryOptions {
  retries?: number;
  backoff?: number;
  jitter?: boolean; // добавить случайный разброс задержки
}

async function fetchWithRetry(
  url: string,
  fetchOptions: RequestInit = {},
  { retries = 3, backoff = 300, jitter = true }: RetryOptions = {}
): Promise<Response> {
  for (let attempt = 0; attempt <= retries; attempt++) {
    try {
      const res = await fetch(url, fetchOptions);
      if (res.status >= 500 && attempt < retries) {
        await delay(calcDelay(backoff, attempt, jitter));
        continue;
      }
      return res;
    } catch (err) {
      if (attempt === retries) throw err;
      await delay(calcDelay(backoff, attempt, jitter));
    }
  }
  throw new Error('Unreachable');
}

function calcDelay(backoff: number, attempt: number, jitter: boolean): number {
  const base = backoff * 2 ** attempt;
  return jitter ? base * (0.5 + Math.random * 0.5) : base;
}

function delay(ms: number) { return new Promise((r) => setTimeout(r, ms)); }

Ключевые моменты

  • Повторяй только 5xx (серверные) — 4xx (клиентские) повторять бессмысленно; 401/403 не станут 200 от повтора.
  • Exponential backoff — каждая попытка ждёт вдвое дольше предыдущей: 300, 600, 1200 мс.
  • Jitter — случайный разброс задержки предотвращает «гром стада»: одновременный retry тысяч клиентов.
  • Устанавливай AbortController timeout чтобы сам fetch не висел бесконечно.

Варианты

  • ky — fetch-обёртка с retry опцией из коробки: ky.get(url, { retry: 3 }).
  • axios-retry — плагин для axios.

Связанные рецепты / темы