> Как реализовать функцию retry для повторного вызова асинхронной функции при ошибке в JavaScript (JavaScript)

Уровень: senior · Роль: frontend · Категория: Технические вопросы

Компании: Яндекс

Стек: JavaScript

> Пример ответа

Короткий ответ

Функция retry для асинхронной функции реализуется через рекурсивный вызов или цикл с try/catch, повторяя вызов при ошибке до достижения лимита попыток. Ключевые моменты: передача оригинальных аргументов, настройка задержки между попытками (exponential backoff), обработка ошибок после исчерпания попыток. Важно корректно обрабатывать reject и не терять контекст вызова.

Подробное объяснение

Основная задача retry - повысить отказоустойчивость при работе с ненадёжными асинхронными операциями (сетевые запросы, API). Реализация строится на обёртке, которая перехватывает reject промиса и повторяет вызов.

Базовый паттерн:

  • Принимает асинхронную функцию, количество попыток, опционально задержку и условие для повторной попытки.
  • Использует рекурсию или цикл for/while.
  • При ошибке проверяет, остались ли попытки, и либо ждёт (через setTimeout или sleep), либо пробрасывает ошибку.

Варианты усложнения:

  • Exponential backoff: увеличение задержки между попытками (например, 1с, 2с, 4с).
  • Jitter: случайное отклонение задержки для избежания thundering herd.
  • Retry condition: повторять только для определённых типов ошибок (например, 5xx, но не 4xx).
  • Max retry timeout: общий лимит времени на все попытки.

На практике

В реальных проектах retry часто используется с fetch, WebSocket, или любыми внешними вызовами. Полезно добавить логирование каждой попытки и финальной ошибки. Для Node.js можно использовать готовые библиотеки (p-retry, async-retry), но понимание реализации важно для кастомизации.

Типичный сценарий: retry для POST-запроса к API, где временные сбои сервера - норма. Важно не повторять идемпотентные операции без проверки.

Пример кода

JAVASCRIPT
async function retry(asyncFn, { attempts = 3, delay = 1000, backoff = 2 } = {}) {
for (let i = 0; i < attempts; i++) {
try {
return await asyncFn();
} catch (error) {
if (i === attempts - 1) throw error;
await new Promise(resolve => setTimeout(resolve, delay * Math.pow(backoff, i)));
}
}
}
// Использование
const fetchWithRetry = () => retry(
() => fetch('https://api.example.com/data'),
{ attempts: 5, delay: 500 }
);

Более гибкая версия с поддержкой retry condition:

JAVASCRIPT
async function retryWithCondition(asyncFn, options = {}) {
const { attempts = 3, delay = 1000, shouldRetry = () => true } = options;
for (let i = 0; i < attempts; i++) {
try {
return await asyncFn();
} catch (error) {
if (i === attempts - 1 || !shouldRetry(error)) throw error;
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}

Как отвечать на собеседовании

Начни с простого определения и базовой реализации. Затем покажи, как добавляешь задержку и exponential backoff. Упомяни edge cases: потеря контекста (this), передача аргументов, обработка синхронных ошибок. Покажи понимание trade-off между retry и circuit breaker. Если спросят про параллельные вызовы - обсуди проблемы с очередностью и idempotency.

Что проверяет интервьюер

  • Понимание асинхронности и промисов.
  • Умение работать с рекурсией/циклами в асинхронном контексте.
  • Знание паттернов отказоустойчивости (retry, backoff, jitter).
  • Внимание к деталям: потеря контекста, проброс ошибок, бесконечные циклы.
  • Способность проектировать гибкие API (опции, колбэки).

Типичные ошибки

  • Бесконечный цикл при неправильном счётчике попыток.
  • Потеря контекста (this) при передаче метода объекта.
  • Игнорирование задержки - мгновенный повтор при ошибке.
  • Неправильная обработка последней ошибки (проброс не той ошибки).
  • Отсутствие проверки типа ошибки - повтор даже для 4xx.
  • Использование рекурсии без учёта стека вызовов (хотя для async/await это не проблема).

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью