Every uncancelled fetch is a future race condition. AbortController is the standard, free, no-library-needed way to cancel an in-flight request when the user changes their mind.
The race condition
A user types in a search box. Each keystroke fires a fetch. The fetches return out of order — the response for 'r' arrives after the response for 're'. Without cancellation, the older response wins (because it lands later in time), and the UI shows results for the wrong query. The fix is to abort superseded requests.
The API
Create a controller, pass its signal to fetch, call controller.abort() when you want to cancel. The fetch promise rejects with an AbortError. Filter that error type out of your error handling (it's expected, not exceptional).
Where to integrate
useEffect — create the controller at the top, abort in the cleanup function (returns from the effect).
Custom hook — same pattern, just inside the hook.
Event handlers — store the controller in a ref so subsequent handler calls can abort the previous one.
Timeouts
For a hard timeout, combine AbortController with AbortSignal.timeout(ms): fetch(url, { signal: AbortSignal.timeout(5000) }) aborts after 5 seconds. For manual + timeout, use AbortSignal.any([ctrl.signal, AbortSignal.timeout(5000)]) — abort if either fires.
Every fetch needs a way to be cancelled. 'I don't need cancellation' is usually 'I haven't hit the race condition yet.' Build the discipline once — every fetch in a hook gets a signal, every cleanup aborts. The bug it prevents is the kind QA can't reproduce.
Code
useEffect — AbortController for fetch lifecycle·tsx
Build a search input wired to useSearchResults. Throttle it with useDebouncedValue (Track 3 lesson 4). Verify that rapid typing doesn't produce flickering wrong results — the AbortController cancels superseded requests, the debounce reduces the rate. Add a 3-second timeout via AbortSignal.any and prove a deliberately slow endpoint shows a timeout error after 3s.
Hint
If you see results from an old query, your AbortController setup is wrong — likely the cleanup isn't running, or the controller is created outside the useEffect (so it's the same one across renders).
Progress
Progress is local-only — sign in to sync across devices.