JavaScript AbortController Table
| Piece | What it does | Field note |
|---|---|---|
new AbortController() | The ownership end | You keep the controller; everything else only ever sees the signal |
controller.signal | The listener wire | Pass to fetch, addEventListener, any count of consumers - one wire, many users |
controller.abort() | The plug pull | Rejects every fetch on the signal with AbortError and detaches every listener on it |
signal.aborted | The state flag | true after abort() - check BEFORE post-processing a response the user stopped waiting for |
signal.addEventListener('abort', fn) | The reactive hook | Cleanup fires the moment cancellation happens, wherever it came from |
e.name === 'AbortError' | The cancel branch | A DOMException, not a crash - the silent path: drop the spinner, return, never show an error toast |
AbortSignal.timeout(ms) | The one-liner timer | Auto-aborts after ms - replaces the setTimeout + controller dance (2022+, all browsers) |
AbortSignal.any([a, b]) | The combiner | Aborts when ANY input signal aborts - merge user-cancel with a deadline into one wire |
AbortController is the cancellation handle for the web platform: build one, hand its signal to a fetch (or an event listener, or any number of them), and call controller.abort() to pull the plug on everything holding that signal. One controller cancels many operations - the signal is a wire, not a one-shot flag.
Bottom line: the AbortError you catch is CONTROL FLOW, not a failure. When the user cancels a search or navigates away, fetch rejects with e.name === 'AbortError' - that path deserves silence (drop the spinner, return), not an error toast. Branching on the error name is what separates polished apps from ones that yell at users for clicking stop.
The honest part: the killer use case is the search-as-you-type race - the user typed a new character, so the old request is garbage; abort it, or a slow stale response can land AFTER the fresh one and overwrite it. Cancellation is how you make out-of-order responses harmless instead of hoping the network cooperates.
How to use
- Cancel a fetch: const c = new AbortController(); fetch(url, { signal: c.signal }) then c.abort() when the user cancels - the fetch promise rejects with a DOMException named AbortError.
- Catch it correctly: catch (e) { if (e.name === 'AbortError') return; throw e; } - treat cancellation as the silent early-exit path and re-throw everything else.
- Cancel event listeners too: addEventListener('resize', fn, { signal: c.signal }) - one abort() detaches every listener on the signal, the modern replacement for tracking remove functions by hand in cleanup code.
- Add a timeout without setTimeout: fetch(url, { signal: AbortSignal.timeout(8000) }) - auto-aborts after 8 seconds, no controller variable, no timer cleanup (supported in all browsers since 2022).
- Combine conditions: AbortSignal.any([userCancelSignal, AbortSignal.timeout(5000)]) aborts when EITHER fires - merge user-initiated cancel with a deadline into one wire.
Frequently asked questions
What is the difference between the controller and the signal?
The controller is the OWNERSHIP end and the signal is the LISTENER end, deliberately split so the ability to cancel can be handed out separately from the ability to trigger cancellation. Code that STARTS work creates the controller and passes only controller.signal to the fetch or listener; code that OWNS the lifecycle (a component, a search box) keeps the controller and calls abort(). You cannot abort through the signal itself - which is exactly what lets an API accept a signal without trusting the caller with the kill switch.
Why does my aborted fetch throw instead of just stopping quietly?
Because abort is modelled as a rejection: the promise needs a terminal state, and 'rejected with AbortError' IS the cancelled state. The design consequence is on you: every abort-aware fetch must catch and branch on e.name === 'AbortError' - return early, update no UI - or your cancellation shows up as a console error and a broken spinner. There is no separate 'oncancel' callback on fetch by design; the catch is the cancel handler.
Can one AbortController cancel multiple fetches or listeners?
Yes - that is the point of the signal design. Pass the same controller.signal to ten fetches and five event listeners; one abort() rejects all ten requests and detaches all five listeners at once. Typical shape: a component creates ONE controller on mount, wires every network call and subscription to its signal, and the cleanup function is a single abort() line.
How do I check whether an abort already happened?
Read signal.aborted - it is true after abort() has been called (or reason thrown). The pattern: check it BEFORE starting expensive post-processing of a response, because a fetch can complete successfully after the user stopped caring; if (signal.aborted) return before parsing or setState saves the wasted work and the state clobber. The abort event on the signal fires at the same moment, for cleanup that must run reactively.