Cancel In-Flight Requests With `AbortController`
Aborting Fetches
Pass a `signal` to `fetch`; cancel it from the effect's cleanup. Saves bandwidth and avoids race conditions at the same time.
What you'll learn
- Use `AbortController` with `fetch`
- Cancel from the effect cleanup
- Handle the resulting `AbortError`
AbortController is a browser API that lets you cancel async work
— including fetch. It pairs perfectly with effect cleanup:
create a controller in the effect, abort it in the cleanup.
The Pattern
function UserCard({ id }) {
const [user, setUser] = useState(null);
useEffect(() => {
const ctrl = new AbortController();
fetch(`/api/users/${id}`, { signal: ctrl.signal })
.then(r => r.json())
.then(setUser)
.catch(err => {
if (err.name === "AbortError") return; // ignore — we cancelled
throw err;
});
return () => ctrl.abort();
}, [id]);
if (!user) return <p>Loading…</p>;
return <p>{user.name}</p>;
} When id changes, the cleanup calls ctrl.abort(). The browser
cancels the in-flight request. The rejected promise produces an
AbortError you ignore.
With async/await
useEffect(() => {
const ctrl = new AbortController();
(async () => {
try {
const res = await fetch(`/api/users/${id}`, { signal: ctrl.signal });
setUser(await res.json());
} catch (err) {
if (err.name !== "AbortError") setError(err);
}
})();
return () => ctrl.abort();
}, [id]); Why Bother Aborting?
Even with a cancelled flag (previous lesson), the browser still
downloads and parses the response. Aborting:
- Saves bandwidth and CPU
- Frees server resources sooner
- Prevents the catch handler from firing with a stale response
For light, fast endpoints this is gravy. For slow or heavy endpoints it’s important.
Handling The Abort Error
AbortError is a normal rejection — fetch doesn’t have a special
“cancelled” outcome. Code that wraps fetch with retries or error
toasts needs to ignore AbortError explicitly.
.catch(err => {
if (err.name === "AbortError") return;
console.error(err);
setError(err);
}) Beyond Fetch
Many APIs accept an AbortSignal:
fetch(url, { signal })addEventListener(event, fn, { signal })— listener auto-removessetTimeoutdoesn’t, but you can roll your own viasignal.addEventListener("abort", ...)
A single controller can manage multiple operations — cancel them
all at once with one .abort().
Up Next
Beyond fetches — subscribing to event sources and websockets.
Subscriptions →