How do you cancel previous API requests when newer ones supersede them?
AbortController is the standard mechanism: create a controller, pass controller.signal to fetch, call controller.abort() to cancel. In React, store the controller in a ref or use the effect cleanup. Abort fires AbortError on the catch. For race protection (responses out of order), combine abort with a request-id guard. React Query / SWR handle this automatically when the query key changes.
Cancellation prevents stale responses from clobbering newer ones and frees up network/CPU resources.
AbortController — the primitive
const controller = new AbortController();
fetch('/api/data', { signal: controller.signal })
.then(r => r.json())
.then(setData)
.catch(err => {
if (err.name === 'AbortError') return; // expected — user cancelled
throw err;
});
// Later: cancel
controller.abort();When .abort() fires, the in-flight fetch rejects with AbortError. The browser tears down the connection (or in HTTP/2, closes the stream); the body never finishes reading.
Pattern 1: cancel previous on new (effect-based)
When fetching driven by an effect dependency (e.g. query prop):
useEffect(() => {
const ctrl = new AbortController();
fetch(`/search?q=${query}`, { signal: ctrl.signal })
.then(r => r.json())
.then(setResults)
.catch(err => { if (err.name !== 'AbortError') setError(err); });
return () => ctrl.abort(); // cleanup runs before next effect — cancels old
}, [query]);Effect cleanup runs before the next effect fires (and before unmount), so changing query always cancels the previous request.
Pattern 2: cancel previous on click (event-based)
When the trigger is an event handler, store the controller in a ref:
const ctrlRef = useRef(null);
async function onClick() {
ctrlRef.current?.abort();
ctrlRef.current = new AbortController();
try {
const res = await fetch('/api/data', { signal: ctrlRef.current.signal });
setData(await res.json());
} catch (err) {
if (err.name !== 'AbortError') throw err;
}
}Pattern 3: request-id guard (defense in depth)
Cancel might race — abort fires but response was already in transit. Add an id check:
const idRef = useRef(0);
async function fetchLatest() {
const id = ++idRef.current;
const res = await fetch('/api/data');
if (id !== idRef.current) return; // stale, ignore
setData(await res.json());
}Combine with abort for both:
async function fetchLatest() {
ctrlRef.current?.abort();
ctrlRef.current = new AbortController();
const id = ++idRef.current;
try {
const res = await fetch('/api/data', { signal: ctrlRef.current.signal });
if (id !== idRef.current) return;
setData(await res.json());
} catch (err) {
if (err.name !== 'AbortError') throw err;
}
}Pattern 4: timeout + cancel together
const signal = AbortSignal.any([
userController.signal,
AbortSignal.timeout(5000),
]);
fetch(url, { signal });Modern (Chrome 116+, Node 20+). One signal aborted by either user or timeout.
In React Query / SWR
Don't write any of the above. The library does it:
const { data } = useQuery({
queryKey: ['search', query],
queryFn: ({ signal }) => fetch(`/search?q=${query}`, { signal }).then(r => r.json()),
});When query changes, the library aborts the previous query, dedupes concurrent identical queries, and discards stale responses.
What gets cancelled
- Network request: connection torn down; server may eventually see RST but typically keeps processing (server-side cancellation is rare in HTTP).
- Body read: if you've already received headers and are streaming the body, abort cancels the stream.
- Pending response parse:
res.json()is its own async — aborting before it finishes leaves the parse to fail with AbortError.
What does NOT cancel
- Server-side work. The server keeps doing whatever it's doing — you've cancelled the client read, not the work.
- Side effects of mutations. If you POSTed and the server already wrote to DB, abort doesn't roll that back.
- Other in-flight requests sharing the connection (HTTP/1 yes, HTTP/2 multiplexed no).
Pitfalls
- Don't auto-retry abort errors — the user (or app) intentionally cancelled.
- Don't show error UI for AbortError — silent or no-op is correct.
- POST cancellation is dangerous — the server may have committed. Treat POST cancel as "best effort"; design for idempotency.
- Forgetting cleanup in event handlers — controller refs that aren't cleared accumulate listeners.
- Reading body after abort —
.json()may reject with a different error mid-parse.
Mental model
Cancellation is for race control and resource hygiene, not for stopping side effects. The server doesn't care that you cancelled. Design your UI assuming the server may have done the work anyway, especially for mutations.
Follow-up questions
- •How does cancellation work server-side?
- •What's AbortSignal.any and when is it useful?
- •How does React Query handle cancellation automatically?
- •Why is cancelling a POST dangerous?
Common mistakes
- •Treating AbortError as a real error and surfacing it in UI.
- •Forgetting to abort on unmount — stale setState warnings.
- •Auto-retrying on AbortError — user/app cancelled intentionally.
- •Cancelling a POST and assuming the server didn't process it.
- •Storing controller in component state — re-renders bust the reference.
- •Not handling the race window (response arrives before abort fires).
Performance considerations
- •Cancellation saves bandwidth (server may stop sending body), browser parse cost, and React re-render churn. For search-as-you-type at scale, cancel + debounce together cut request rate ~80%. Server-side, request cancellation reduces wasted work only if the server checks the connection (rare in most stacks).
Edge cases
- •Pre-aborted signals: AbortController with abort() called before fetch — fetch rejects immediately.
- •Cross-realm signals (iframe/worker) — propagate via structured cloning.
- •Streaming responses (SSE, ReadableStream) — abort cancels the stream mid-read.
- •Service workers can intercept and serve cached responses; abort cancels the cache lookup too.
- •AbortSignal.any not yet on older browsers — feature-detect and fall back.
Real-world examples
- •GitHub omnisearch debounces input and cancels stale requests.
- •Algolia/Typesense client libraries cancel in-flight queries on next keystroke.
- •React Query, SWR, Apollo all handle cancellation internally.