What the pair does
AbortController is a producer of cancellation events.
AbortSignal is the consumer side. Code that wants to be
cancellable accepts a signal:
const controller = new AbortController();
const { signal } = controller;
const res = await fetch('/api/users', { signal });
// Elsewhere
controller.abort(new Error('user cancelled'));
abort(reason?) flips the signal’s aborted flag, fires its
abort event, and notifies every observer that’s listening.
The pair is defined in DOM Living Standard (§3.2 Abort), not in a separate spec — reflecting that cancellation is a DOM primitive, not a Fetch primitive.
Where it works
Since 2019, signal acceptance has spread across the platform:
fetch(url, { signal })— the original integration (2019).addEventListener(type, fn, { signal })— auto-removes the listener when the signal aborts (Chromium 90, 2021; cross-engine by 2022).ReadableStream.cancel(reason)— accepts a signal indirectly via the consumer.EventTarget.dispatchEventdoes not — events are synchronous.setTimeout(fn, ms, { signal })— proposal, not yet shipped.crypto.subtle.digest({ name: 'SHA-256' }, data, { signal })— proposed, partial.
About 90% of cancellable async APIs added between 2019 and 2024 accept a signal.
Composing signals
AbortSignal.any([s1, s2, ...]) (cross-engine 2024) returns a
signal that aborts when any input aborts. Useful for combining a
user-cancel signal with a timeout:
const userCancel = new AbortController();
const timeout = AbortSignal.timeout(5000);
const combined = AbortSignal.any([userCancel.signal, timeout]);
const res = await fetch(url, { signal: combined });
AbortSignal.timeout(ms) (cross-engine 2023) returns a signal
that aborts after the given milliseconds with a
TimeoutError DOMException.
AbortSignal.abort(reason?) is the static factory for an
already-aborted signal — useful for tests or for short-circuiting
a pre-cancelled flow.
Reading the abort reason
After abort, signal.reason holds the value passed to
abort(reason). Code that wants useful error messages should
pass concrete Error instances:
controller.abort(new DOMException('User clicked cancel', 'AbortError'));
Catching code should distinguish a deliberate cancel from a network failure:
try {
await fetch(url, { signal });
} catch (err) {
if (err.name === 'AbortError') return; // expected
throw err; // unexpected
}
Fetch wraps the abort reason in an AbortError exception by
spec; comparing err.name === 'AbortError' is the portable
pattern.
addEventListener integration
The signal option on addEventListener removes the boilerplate
“remember the listener and call removeEventListener”:
const controller = new AbortController();
button.addEventListener('click', onClick, { signal: controller.signal });
window.addEventListener('resize', onResize, { signal: controller.signal });
// Later — one call removes both
controller.abort();
In a custom-element teardown, this pattern reduces about 30% of typical event-binding code.
Common patterns
Per-request cancellation
let inflight;
async function search(q) {
inflight?.abort();
inflight = new AbortController();
try {
const res = await fetch(`/search?q=${encodeURIComponent(q)}`,
{ signal: inflight.signal });
return await res.json();
} catch (e) {
if (e.name !== 'AbortError') throw e;
}
}
Each new search aborts the previous request; the response of an older query never overrides a newer one.
Component lifetime
class WikiSearch extends HTMLElement {
#ac = new AbortController();
connectedCallback() {
this.addEventListener('input', this.#onInput, { signal: this.#ac.signal });
document.addEventListener('keydown', this.#onKey, { signal: this.#ac.signal });
}
disconnectedCallback() { this.#ac.abort(); }
}
The pattern survives multiple connect/disconnect cycles by
reassigning #ac on connect.
Common pitfalls
- Calling
abort()twice. Idempotent; safe but no-op. - Listening for
abortevent via signal.addEventListener(“abort”, fn) without setting{ once: true }— the listener leaks. - Reading
signal.reasonbefore abort. Returnsundefined. - Checking
signal.abortedand proceeding asynchronously. Race condition: signal might abort between the check and the next microtask. Always pass the signal into the cancellable API rather than checking and proceeding. - No handler for
AbortErrorrejection. Logs as an unhandled-rejection warning even when cancellation is expected.
Cross-engine support
Core AbortController/AbortSignal reached interop in 2019.
AbortSignal.timeout() reached interop in 2023.
AbortSignal.any() reached interop in 2024. The
addEventListener signal option reached interop in 2022.
Web Platform Tests for AbortController report about 99% pass-rate across Chromium (Blink), WebKit, and Gecko as of the 2024 Interop dashboard.
Further reading
- DOM Living Standard, §3.2 AbortController.
- Jake Archibald’s
AbortController.anydeep-dive is the canonical author-facing reference for signal composition. - The WHATWG fetch and abort post-mortem documents the 2017 design discussion.
- A proposal to add
signaltosetTimeoutis under discussion as of 2024.