async помилки в useEffect
1) Базовий патерн: IIFE + try/catch/finally
javascript
useEffect(() => {
let cancelled = false;
(async () => {
try {
setLoading(true);
const res = await fetch("/api/items");
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
if (!cancelled) setItems(data);
} catch (e) {
// тут логуємо і виставляємо помилку в стейт
const err = e instanceof Error ? e : new Error(String(e));
if (!cancelled) setError(err);
// відправка в моніторинг (Sentry/GlitchTip)
// reportError(err)
} finally {
if (!cancelled) setLoading(false);
}
})();
return () => { cancelled = true; };
}, [/* deps */]);Навіщо прапорець cancelled? Щоб не викликати setState після анмаунту/зміни запиту.
2) Скасовуємо запити коректно: AbortController
javascript
useEffect(() => {
const controller = new AbortController();
(async () => {
try {
setLoading(true);
const res = await fetch(`/api/items`, { signal: controller.signal });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
setItems(data);
} catch (e) {
// ігноруємо скасування
if (e instanceof DOMException && e.name === "AbortError") return;
const err = e instanceof Error ? e : new Error(String(e));
setError(err);
// reportError(err)
} finally {
setLoading(false);
}
})();
return () => controller.abort();
}, [/* deps */]);Плюси: відсутність гонок при зміні залежностей, безпечне скасування в React 18 (Strict Mode викликає ефекти двічі в dev).
3) Валідуйте відповідь і явно кидайте помилки
javascript
const res = await fetch(url, { signal });
if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`);
const json = await res.json();
if (!Array.isArray(json)) throw new Error("Unexpected payload shape");Інакше "тихі" помилки перетворяться на непередбачувані баги.
4) Ретраї з експоненційною паузою (без нескінченних циклів)
javascript
async function withRetry<T>(fn: () => Promise<T>, attempts = 3) {
let lastErr: unknown;
for (let i = 0; i < attempts; i++) {
try { return await fn(); }
catch (e) {
lastErr = e;
await new Promise(r => setTimeout(r, 2 ** i * 300)); // 300ms, 600ms, 1200ms...
}
}
throw lastErr;
}
useEffect(() => {
const controller = new AbortController();
(async () => {
try {
setLoading(true);
const data = await withRetry(
() => fetch(url, { signal: controller.signal }).then(r => {
if (!r.ok) throw new Error(`HTTP ${r.status}`);
return r.json();
}),
3
);
setData(data);
} catch (e) {
if (e instanceof DOMException && e.name === "AbortError") return;
setError(e as Error);
} finally {
setLoading(false);
}
})();
return () => controller.abort();
}, [url]);5) Стежте за залежностями ефекту
- Усе, що використовується всередині ефекту і змінюється з рендерами, повинне бути в deps: URL, токени, фільтри тощо.
- Якщо передаєте колбеки/об'єкти, мемоізуйте (
useCallback,useMemo), щоб не смикати ефект зайвий раз.
6) Гонки запитів: "актуалізуй тільки останній"
Якщо неможливо використати AbortController (не всі клієнти вміють), зберігайте "id запиту":
javascript
useEffect(() => {
let requestId = Symbol();
const current = requestId;
(async () => {
try {
const data = await api.load(params);
if (current === requestId) setData(data);
} catch (e) {
if (current === requestId) setError(e as Error);
}
})();
return () => { requestId = Symbol(); };
}, [params]);7) Де показувати помилку
- Локальний
error-стейт -> показати inline-алерт/кнопку Retry. - Глобальні критичні - репортити в Sentry/GlitchTip.
- Error Boundary для рендер-помилок UI; не для async - їх ловимо тут же, в ефекті.
8) Практичні дрібниці
- У TypeScript оголошуйте
catch (e: unknown)і нормалізуйте доError. - У
finallyзавжди вимикайтеloading. - Для повторних завантажень тримайте
reload()(змінюйте ключ або інкрементуйтеversionу deps). - Обнуляйте попередню помилку перед новою спробою (
setError(null)), щоб UI не "залипав".
9) Коли краще не писати все вручну
Для даних ідеально підходять TanStack Query / RTK Query:
- автоматичний кеш, ретраї, скасування, дедуплікація запитів, статус
isLoading/error/data. - Менше коду в ефектах, менше шансів помилитися.
Коротка відповідь
Для співбесідиPremium
Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.