0Pricing
React Academy · Lesson

Racing Conditions & Effect Cleanup

Use AbortController in useEffect to cancel stale fetch requests on re-render.

Racing Conditions & Effect Cleanup is a free React Academy lesson on CoddyKit — lesson 1 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the React Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

Welcome

In this lesson you will learn how race conditions happen in React effects that fetch data, and how to eliminate them using AbortController for cleanup.

What Is a Race Condition?

A race condition occurs when two async operations complete in an unpredictable order. In React, if a user navigates quickly between items, a slow fetch for item A may resolve after item B's fetch, showing stale data.

The Bug: Stale Fetch Response

Without cleanup, if the component re-renders with a new ID before the old fetch completes, both fetches run. The older, slower fetch may overwrite newer data in state.
useEffect(() => {
  fetch('/api/item/' + id)
    .then(r => r.json())
    .then(data => setData(data)); // may run AFTER a newer fetch!
}, [id]);

Fix 1: Ignore Flag

A simple fix: set a boolean `ignore` flag in the effect. The cleanup function sets it to true. The fetch callback checks the flag before calling setState.
useEffect(() => {
  let ignore = false;
  fetch('/api/item/' + id)
    .then(r => r.json())
    .then(data => { if (!ignore) setData(data); });
  return () => { ignore = true; };
}, [id]);

Fix 2: AbortController

AbortController is the modern standard. Create a controller in the effect, pass its signal to fetch, and abort it in the cleanup function. The fetch throws an AbortError that you can ignore.
useEffect(() => {
  const controller = new AbortController();
  fetch('/api/item/' + id, { signal: controller.signal })
    .then(r => r.json())
    .then(data => setData(data))
    .catch(err => {
      if (err.name === 'AbortError') return; // expected
      setError(err.message);
    });
  return () => controller.abort();
}, [id]);

AbortController with async/await

Use a try/catch with async functions inside useEffect. Ignore AbortError in the catch block.
useEffect(() => {
  const ctrl = new AbortController();
  async function load() {
    try {
      const res = await fetch('/api/' + id, { signal: ctrl.signal });
      const json = await res.json();
      setData(json);
    } catch (e) {
      if (e.name !== 'AbortError') setError(e.message);
    }
  }
  load();
  return () => ctrl.abort();
}, [id]);

Cleanup Is Always Required

Even when race conditions are unlikely (e.g. a one-time fetch on mount), always add cleanup. In React 18 StrictMode, effects fire twice — without cleanup, the first abandoned request pollutes state.

The useEffect Async Gotcha

useEffect's callback cannot be `async`. Define an async function inside the effect and call it immediately — or use a regular function with .then()/.catch().
useEffect(() => {
  // useEffect callback is NOT async
  async function fetchData() { /* ... */ }
  fetchData(); // call it
  return () => ctrl.abort();
}, [id]);

Libraries Handle This For You

React Query, SWR, and RTK Query all handle race conditions, abort, and cleanup automatically. When using these libraries, you don't need to write AbortController code manually.

Testing Race Conditions

To test race conditions manually: open Network DevTools, throttle to 'Slow 3G', and quickly click between items. Without cleanup, you will see stale data flash. With AbortController, only the last selected item shows.

Quick Check

What does calling controller.abort() in a useEffect cleanup function do?

Recap

Race conditions occur when a stale fetch response updates state after a newer fetch. Fix with an ignore flag or AbortController. Always abort or ignore in useEffect cleanup. Libraries like React Query handle this automatically.

Up Next

Next lesson: **useTransition for Non-Blocking UI Updates** — you will wrap expensive state updates in startTransition to keep the UI responsive.

Frequently asked questions

Is the “Racing Conditions & Effect Cleanup” lesson free?

Yes — the full text of “Racing Conditions & Effect Cleanup” is free to read here on the web, and the React Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the React Academy course, upgrade to CoddyKit PRO.

What will I learn in “Racing Conditions & Effect Cleanup”?

Use AbortController in useEffect to cancel stale fetch requests on re-render. You practise React Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start React Academy?

No prior experience is required. React Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 1 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “Racing Conditions & Effect Cleanup” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this React Academy lesson?

Yes. Every React Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. Racing Conditions & Effect Cleanup
  2. useTransition for Non-Blocking UI Updates
  3. useDeferredValue for Input Debouncing
  4. Suspense for Data & Code Boundaries
← Back to React Academy