Какой тип данных должна возвращать функция retry в JavaScript
Что проверяют
Понимаете ли вы, что снаружи retry должна выглядеть так же, как одна асинхронная попытка. Следом почти всегда просят добавить задержку между попытками и объяснить, какие ошибки повторять нельзя.
Как отвечать
retry возвращает Promise, который выполняется результатом первой успешной попытки или отклоняется последней ошибкой, когда попытки кончились. В TypeScript это retry<T>(fn: () => Promise<T>, options): Promise<T>: вызывающий код пишет await и не знает, сколько было попыток.
Принимать нужно фабрику, то есть функцию, которая при каждом вызове запускает операцию заново. Готовый промис уже выполняется, перезапустить его нельзя.
async function retry(fn, { retries = 3, baseDelay = 300 } = {}) {
for (let attempt = 0; ; attempt++) {
try {
return await fn();
} catch (err) {
if (attempt >= retries) throw err;
const delay = baseDelay * 2 ** attempt;
await new Promise((r) => setTimeout(r, delay + Math.random() * delay));
}
}
}К коду стоит проговорить:
- экспоненциальная задержка с джиттером: паузы растут, а случайная добавка не даёт тысяче клиентов повторить запрос в одну миллисекунду;
- что повторять: сетевые сбои, 5xx, 429 с учётом
Retry-After. Ответы 400, 401, 404 повторять бессмысленно; - отмена:
AbortSignalв параметрах, чтобы уход со страницы останавливал и попытки, и ожидание.
В проекте повтор часто уже есть: TanStack Query по умолчанию повторяет упавший запрос три раза с растущей паузой.
Где спотыкаются
Забывают, что fetch отклоняет промис только на сетевой ошибке, а не на 404 или 500. Без проверки response.ok и явного throw ошибка сервера для retry выглядит успехом.
Повторяют неидемпотентный POST. Если запрос дошёл, а потерялся только ответ, повтор создаст второй заказ: нужен ключ идемпотентности или отказ от повтора.
Глотают ошибку и возвращают null. Вызывающий код не отличит «данных нет» от «сервер лежит», поэтому после последней попытки промис должен отклоняться.