Skip to main content

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

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.