How do you cancel API requests with AbortController
Create an AbortController, pass controller.signal to fetch, call controller.abort() to cancel. The fetch rejects with an AbortError you should catch and ignore. In React, abort in the useEffect cleanup to kill stale requests and prevent setState-after-unmount and out-of-order responses.
AbortController is the standard way to cancel an in-flight fetch (and other abortable APIs).
The mechanism
const controller = new AbortController();
fetch("/api/data", { signal: controller.signal })
.then((res) => res.json())
.then((data) => { /* use data */ })
.catch((err) => {
if (err.name === "AbortError") return; // expected — ignore it
throw err; // real error
});
// cancel it:
controller.abort();new AbortController()gives you acontrollerand acontroller.signal.- Pass
signaltofetch(and to anything else that supports it —addEventListener, axios, etc.). controller.abort()aborts: the fetch promise rejects with anAbortError.- A controller is single-use — once aborted, make a new one for the next request.
Catch the AbortError
An aborted fetch rejects — so you must catch and explicitly ignore AbortError, or it looks like a real failure (error toast, etc.).
The React pattern — abort in cleanup
This is the main reason it comes up. In a useEffect that fetches, abort the request in the cleanup function:
useEffect(() => {
const controller = new AbortController();
fetch(`/api/search?q=${query}`, { signal: controller.signal })
.then((r) => r.json())
.then(setResults)
.catch((e) => { if (e.name !== "AbortError") setError(e); });
return () => controller.abort(); // cancel on unmount OR when query changes
}, [query]);This solves two real bugs:
- setState-after-unmount — if the component unmounts before the response, the request is cancelled instead of resolving into a dead component.
- Out-of-order responses (race condition) — when
querychanges fast, each new effect run aborts the previous request, so a slow old response can't overwrite a newer one.
More
- Timeouts —
AbortSignal.timeout(5000)aborts automatically after 5s. - Combine signals —
AbortSignal.any([...])to abort on whichever fires first. - One controller, many requests — passing the same signal to several fetches cancels them all together.
The framing
"Make an AbortController, pass its signal to fetch, call abort() to cancel — the fetch then rejects with an AbortError you catch and ignore. The big use is in React: abort in the useEffect cleanup, which both prevents setState-after-unmount and kills the out-of-order-response race when a dependency changes rapidly. Controllers are single-use, and AbortSignal.timeout gives you cancellation-by-timeout for free."
Follow-up questions
- •Why must you catch and ignore the AbortError?
- •How does aborting in useEffect cleanup prevent race conditions?
- •How would you implement a request timeout with AbortController?
- •Can one AbortController cancel multiple requests?
Common mistakes
- •Not catching AbortError, so cancellation looks like a real failure.
- •Reusing an aborted controller — it's single-use.
- •Forgetting to abort in useEffect cleanup, leaving race conditions and unmount warnings.
- •Thinking abort() stops the server work — it only stops the client waiting.
Performance considerations
- •Cancelling stale requests frees the connection and avoids processing responses you'll discard — it reduces wasted parsing/rendering and prevents janky out-of-order UI updates.
Edge cases
- •Request already completed when abort() is called — no-op.
- •Aborting before fetch even starts.
- •Non-fetch APIs that don't support signal.
- •abort() doesn't undo server-side side effects already triggered.
Real-world examples
- •Search-as-you-type aborting the previous query's request.
- •React Query and SWR using AbortController internally to cancel superseded queries.