Suggest an editImprove this articleRefine the answer for “async errors in useEffect”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)Async code inside `useEffect` is wrapped in an IIFE with `try/catch/finally`, requests are canceled with `AbortController`, and a flag (a `cancelled` boolean or a request id) is used so `setState` is not called after unmount or after the dependencies change. **Key point:** ignore the `AbortError` (it is an expected cancellation), and normalize everything else to an `Error` before putting it into the `error` state.Shown above the full answer for quick recall.Answer (EN)Image## 1) The basic pattern: 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) { // log here and put the error into state const err = e instanceof Error ? e : new Error(String(e)); if (!cancelled) setError(err); // send to monitoring (Sentry/GlitchTip) // reportError(err) } finally { if (!cancelled) setLoading(false); } })(); return () => { cancelled = true; }; }, [/* deps */]); ``` **Why the** `cancelled` **flag?** So `setState` is not called after unmount/after the request changes. --- ## 2) Canceling requests correctly: `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) { // ignore the cancellation 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 */]); ``` **Benefits:** no race conditions when dependencies change, safe cancellation in React 18 (Strict Mode runs effects twice in dev). --- ## 3) Validate the response and explicitly throw errors ```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"); ``` Otherwise "silent" errors turn into unpredictable bugs. --- ## 4) Retries with exponential backoff (without infinite loops) ```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) Watch the effect's dependencies - Everything used inside the effect that changes across renders must be in deps: URL, tokens, filters, and so on. - If you pass callbacks/objects, **memoize** them (`useCallback`, `useMemo`) so the effect does not fire needlessly. --- ## 6) Request races: "only keep the latest one current" If `AbortController` cannot be used (not every client supports it), keep a "request 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) Where to show the error - A local `error` state -> show an inline alert/a Retry button. - Global critical ones - report to Sentry/GlitchTip. - An **Error Boundary** for render errors in the UI; **not** for async - those are caught right there, in the effect. --- ## 8) Practical details - In TypeScript, declare `catch (e: unknown)` and normalize it to an `Error`. - Always turn off `loading` in `finally`. - For reloading, keep a `reload()` (change a key, or increment a `version` in deps). - Reset the previous error before a new attempt (`setError(null)`), so the UI does not get "stuck". --- ## 9) When it is better not to write all of this by hand **TanStack Query / RTK Query** are a great fit for data: - automatic cache, retries, cancellation, request deduplication, an `isLoading/error/data` status. - Less code in effects, fewer chances to get it wrong.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.