AbortController
Cancel fetch and other async work with AbortController and AbortSignal — timeouts, race conditions, and cleanup when components unmount.
- javascript
- abortcontroller
- fetch
- async
Search boxes that fire a request per keystroke create a classic race: response B can land after A and paint stale data. AbortController is the browser-native way to cancel that work — not by ignoring the result, but by actually aborting the underlying request.
The API is two pieces: a controller you own, and a signal you pass to anything that supports cancellation (fetch, streams, some libraries).
The minimal shape
const controller = new AbortController();
const { signal } = controller;
fetch('/api/search?q=react', { signal })
.then((r) => r.json())
.then(console.log)
.catch((err) => {
if (err.name === 'AbortError') return; // expected cancel
throw err;
});
// later: user typed again, or navigated away
controller.abort();
Calling abort() rejects the fetch promise with an AbortError (DOMException). One controller aborts once; create a new one for the next request.
Search race (the production pattern)
let controller = null;
async function search(query) {
controller?.abort();
controller = new AbortController();
try {
const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`, {
signal: controller.signal,
});
if (!res.ok) throw new Error(String(res.status));
return await res.json();
} catch (err) {
if (err.name === 'AbortError') return null; // superseded
throw err;
}
}
Each new call kills the previous in-flight request. Only the latest response is allowed to update UI state.
Timeout without inventing a timer API
AbortSignal.timeout(ms) (modern browsers) aborts after a deadline:
const res = await fetch('/api/slow', {
signal: AbortSignal.timeout(5000),
});
Combine signals with AbortSignal.any([...]) when you need “user cancel or timeout”:
const user = new AbortController();
const signal = AbortSignal.any([
user.signal,
AbortSignal.timeout(8000),
]);
fetch('/api/data', { signal });
// user.abort() still works
Not only fetch
Anything that accepts a signal can cooperate:
function sleep(ms, { signal } = {}) {
return new Promise((resolve, reject) => {
if (signal?.aborted) {
reject(signal.reason ?? new DOMException('Aborted', 'AbortError'));
return;
}
const id = setTimeout(resolve, ms);
signal?.addEventListener(
'abort',
() => {
clearTimeout(id);
reject(signal.reason ?? new DOMException('Aborted', 'AbortError'));
},
{ once: true },
);
});
}
In React, abort on unmount or when effect deps change:
useEffect(() => {
const c = new AbortController();
loadUser(id, { signal: c.signal }).then(setUser).catch(handle);
return () => c.abort();
}, [id]);
Footguns
| Mistake | Reality |
|---|---|
| Swallowing all errors | Only skip AbortError / err.name === 'AbortError' |
| Reusing one controller forever | After abort, the signal stays aborted — new request needs a new controller |
| Assuming abort is instant on the server | Client stops listening; server may still finish work unless it also observes cancel |
| Forgetting streams | response.body readers also take signals in modern APIs |
signal.aborted and signal.reason let you check state without waiting for a rejection. Listen with signal.addEventListener('abort', ...).
Interview answer (30 seconds)
“AbortController gives you an AbortSignal you pass into fetch (or your own async helpers). Calling abort() rejects pending work with AbortError so you can cancel stale searches, unmount requests, and timeouts. Always create a fresh controller per logical operation and treat AbortError as control flow, not a bug.”
Related
AbortSignal.reason and errors
const c = new AbortController();
c.abort(new Error('user cancelled'));
c.signal.reason; // Error: user cancelled
Libraries should prefer signal.throwIfAborted() (where available) at the start of async work so they fail fast without starting network I/O. When wrapping non-fetch APIs, always check signal.aborted before expensive setup and register an abort listener that cancels timers, readers, or XHR (xhr.abort()).
In concurrent React 18+, effects remount in Strict Mode — your abort-on-cleanup path will run twice in development; design loaders so double-abort is harmless.
Further reading
Related guides
- Fetch API FundamentalsHow fetch really works — Response.ok, one-shot bodies, JSON errors, AbortController, credentials, and production footguns.
- FormData and File UploadsBuild multipart uploads with FormData, append files, inspect entries, and pair with fetch — progress and size limits included.
- PromisesSettlement, chaining, errors, Promise API helpers, and how promises plug into the microtask queue — without cargo-cult async.
- Array find, some, every, includesShort-circuiting array predicates: find, findIndex, some, every, and includes — when to use each and common interview traps.
- Array map, filter, reducemap, filter, and reduce as the core transform toolkit — immutability, chaining costs, reduce patterns, and when a plain loop is clearer.