Short answer
Keep the effect itself synchronous. Declare the async function inside it, call it right away, and return a cleanup that sets an ignore flag (or aborts the request), so a slow response from an earlier render can’t overwrite newer data. In Next.js App Router or other frameworks with server data loading, fetching in the framework is often simpler than in an effect.
Why an async effect breaks, and the fix
01
The effect itself is async
“You can also rewrite using the async / await syntax, but you still need to provide a cleanup function” React, useEffect
The Promise an async effect returns is treated as its cleanup, which React can’t call. In development that gives the warning; in production there is no warning, and leaving the screen can crash with destroy is not a function. Move the async work into a function inside the effect.
// warns useEffect(async () => { const res = await fetch(`/api/orders/${id}`); setOrders(await res.json()); }, [id]); // works useEffect(() => { let ignore = false; async function load() { const res = await fetch(`/api/orders/${id}`); const data = await res.json(); if (!ignore) setOrders(data); } load(); return () => { ignore = true; }; }, [id]);02
Responses that arrive out of order
“This ensures your code doesn’t suffer from “race conditions”: network responses may arrive in a different order than you sent them.” React, useEffect
Without cleanup, switching quickly from one id to another can show the older response last. The ignore flag, or an AbortController whose abort() runs in the cleanup, drops the stale one.
useEffect(() => { const controller = new AbortController(); fetch(`/api/orders/${id}`, { signal: controller.signal }) .then((res) => res.json()) .then(setOrders) .catch((err) => { if (err.name !== 'AbortError') setError(err); }); return () => controller.abort(); }, [id]);03
Errors that vanish
“Your setup function may also optionally return a cleanup function.” React, useEffect
An unawaited Promise that rejects inside an effect doesn’t reach an error boundary. Catch the error in the async function and put it in state, so the screen can show it. Any cleanup you return must be a function, never a Promise.
04
Data fetching that belongs to the framework
Effects run after the page renders, so data fetched there shows up late and isn’t in the server HTML. In Next.js, load data in a Server Component or the framework’s data functions; on the client, a data library such as TanStack Query handles caching and races for you.
Checked against the vendor’s docs on : React reference: useEffect (fetching data with Effects); React: Removing Effect Dependencies.
Rather hand it over?
Fixed scope, fixed price, paid upfront. The work happens in writing; no call needed.
- from $450App Fix TicketOne reproducible bug in an existing mobile or web app, diagnosed and fixed.See scope and start
- from $450AI-Built App RescueA Lovable, Bolt or Cursor app checked end to end in 3 days, then its worst problems fixed at a fixed price.No issues worth fixing, no charge. If the Launch Check finds nothing rated medium or higher, Simon refunds the full $450. You keep the report either way. This applies to the first 10 Launch Checks bought after October 30, 2026.See scope and start
Related: React “useEffect has a missing dependency” · React “Maximum update depth exceeded” · Next.js hydration error