JavaScript AbortController Table

PieceWhat it doesField note
new AbortController()The ownership endYou keep the controller; everything else only ever sees the signal
controller.signalThe listener wirePass to fetch, addEventListener, any count of consumers - one wire, many users
controller.abort()The plug pullRejects every fetch on the signal with AbortError and detaches every listener on it
signal.abortedThe state flagtrue after abort() - check BEFORE post-processing a response the user stopped waiting for
signal.addEventListener('abort', fn)The reactive hookCleanup fires the moment cancellation happens, wherever it came from
e.name === 'AbortError'The cancel branchA DOMException, not a crash - the silent path: drop the spinner, return, never show an error toast
AbortSignal.timeout(ms)The one-liner timerAuto-aborts after ms - replaces the setTimeout + controller dance (2022+, all browsers)
AbortSignal.any([a, b])The combinerAborts when ANY input signal aborts - merge user-cancel with a deadline into one wire
Reference: the MDN AbortController guide. The controller-signal split is deliberate ownership design: code that STARTS work creates the controller and hands out only the signal, so an API can accept cancellation without trusting the caller with the kill switch - you cannot abort through a signal, only observe it. The search-as-you-type race is the canonical use: the user typed a new character, the old request is garbage, and aborting it makes out-of-order responses harmless - otherwise a slow stale response lands AFTER the fresh one and overwrites it.
Bottom line: AbortError is control flow, not failure. Every abort-aware fetch needs the branch - catch, check e.name === 'AbortError', return silently - because cancellation is the user saying stop, and answering with an error message is the app talking back. The underrated second act: listener cleanup - addEventListener(..., { signal }) means one abort() line replaces the whole hand-tracked list of removeEventListener calls in component teardown.
Related tools: the fetch table (the request API the signal plugs into), the event listeners table (the signal-option cleanup pattern), the timers table (the setTimeout-plus-abort dance that AbortSignal.timeout replaces), the WebSocket table (close vs abort semantics for sockets), and the SSE table (EventSource with its own abort handling).

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

  1. 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.
  2. 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.
  3. 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.
  4. 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).
  5. 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.

Related tools