fetch за замовчуванням не має тайм-ауту: запит до сервера, що завис, може «висіти» хвилинами. А тимчасові збої мережі чи відповідь 503 часто зникають після повтору.
Тайм-аут через AbortSignal.timeout():
try {
const response = await fetch('/api/report', { signal: AbortSignal.timeout(5000) });
} catch (error) {
if (error.name === 'TimeoutError') {
showError('Сервер не відповів за 5 секунд');
}
}
При тайм-ауті Promise відхиляється з TimeoutError, а при ручному скасуванні через AbortController - з AbortError. Це дає змогу розрізнити причини.
Поєднати тайм-аут і ручне скасування (користувач пішов зі сторінки) - AbortSignal.any():
const controller = new AbortController();
const signal = AbortSignal.any([controller.signal, AbortSignal.timeout(5000)]);
Повтор з експоненційною затримкою і випадковим розкидом:
async function fetchWithRetry(url, options = {}, { retries = 3, baseDelay = 300 } = {}) {
for (let attempt = 0; ; attempt++) {
try {
const response = await fetch(url, { ...options, signal: AbortSignal.timeout(5000) });
if (response.status < 500 && response.status !== 429) {
return response; // успіх або помилка клієнта - не повторюємо
}
if (attempt >= retries) return response;
} catch (error) {
if (attempt >= retries) throw error;
}
const delay = baseDelay * 2 ** attempt * (0.5 + Math.random());
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
Що важливо:
- повторювати лише те, що має сенс повторювати: мережеві помилки, тайм-аути, 502/503/504, 429. Помилки 4xx (валідація, 403, 404) повтор не виправить;
- ідемпотентність:
GETповторювати безпечно.POSTна створення замовлення - ні: перша спроба могла дійти до сервера, а загубилася лише відповідь. Для таких запитів потрібен ключ ідемпотентності, який сервер перевіряє; - розкид затримки (jitter) потрібен, щоб тисячі клієнтів після збою не повторювали запити синхронно й не «добили» сервер, що відновлюється;
Retry-Afterу відповіді 429/503 варто поважати замість власної затримки;- загальний бюджет часу важливіший за кількість спроб: користувач не чекатиме хвилину на три повтори з тайм-аутом по 20 секунд.