AbortController
Reviewed & published by Brayan K
AbortController is a built-in object that produces a reusable signal you hand to async work, so calling its abort() method can cancel a fetch, timer, or any operation listening to that signal.
Part of the free JavaScript course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
It is how you stop work you no longer need: cancelling a stale search request, tearing down a fetch when a component unmounts, or enforcing a hard timeout.
What You'll Learn in This Lesson
- Create a controller and its signal
- Cancel a fetch with abort()
- Catch and identify the AbortError
- Listen to the 'abort' event
- Use AbortSignal.timeout for deadlines
- Know when NOT to reuse a controller
📚 Prerequisites: You should know promises and async/await with try/catch, since aborting surfaces as a rejected promise.
🔔 Real-World Analogy: An AbortController is a fire alarm. The controller is the pull handle; the signal is the wiring that runs to every room. You give the signal to each async task ("listen for the alarm"). Pulling the handle (abort()) rings everywhere at once and every task that was listening stops what it is doing.
1️⃣ The Controller and Its Signal
Create a controller with new AbortController(). It exposes a read-only signal that you pass to the operation you want to be able to cancel, and an abort() method that flips that signal to the aborted state. The signal has an aborted boolean and fires an "abort" event when triggered.
const controller = new AbortController();
const { signal } = controller;
// React the moment the signal is tripped
signal.addEventListener("abort", () => {
console.log("abort event! reason:", signal.reason);
});
console.log("before:", signal.aborted);
// ✅ Expected output: before: false
controller.abort("user navigated away");
console.log("after:", signal.aborted);
// ✅ Expected output: abort event! reason: user navigated away
// ✅ Expected output: after: true2️⃣ Cancelling Async Work & Catching AbortError
Pass signal to fetch and call abort() to cancel the request. The fetch promise rejects with an AbortError, which you catch and distinguish from a real network failure by checking err.name. This is the classic "cancel the previous search as the user types" pattern:
⚠️ Always check err.name: Treating an AbortError as a real error shows users scary "request failed" messages for something they (or your code) caused on purpose.
The runnable demo below uses a cancellable delay that listens to the signal — exactly the shape fetch follows — so you can watch an abort reject with an AbortError in Node:
// A cancellable wait that rejects with an AbortError when the signal trips
function delay(ms, signal) {
return new Promise((resolve, reject) => {
if (signal.aborted) {
return reject(new DOMException("Aborted", "AbortError"));
}
const id = setTimeout(resolve, ms);
signal.addEventListener("abort", () => {
clearTimeout(id);
reject(new DOMException("Aborted", "AbortError"));
});
});
}
async function main() {
const controller = new AbortController();
// Cancel the 1000ms wait after only 20ms
setTimeout(() => controller.abort(), 20);
try {
await delay(1000, controller.signal);
console.log("finished waiting"); // never runs
} catch (err) {
if (err.name === "AbortError") {
console.log("cancelled:", err.name);
// ✅ Expected output: cancelled: AbortError
} else {
throw err;
}
}
}
main();🎯 Your Turn #1 — Trip the Signal
Replace the blank with the method that cancels the controller so the event fires.
const controller = new AbortController();
controller.signal.addEventListener("abort", () => {
console.log("stopped!"); // ✅ Expected output: stopped!
});
// 👉 replace ___ with the method that aborts the controller
controller.___();
console.log(controller.signal.aborted); // ✅ Expected output: true3️⃣ AbortSignal.timeout — Deadlines for Free
When you only need a deadline, skip the controller entirely. AbortSignal.timeout(ms) returns a signal that auto-aborts after ms milliseconds. There is also AbortSignal.any([...]) to abort as soon as any of several signals fires — handy for "cancel on either a timeout or the user clicking stop".
function delay(ms, signal) {
return new Promise((resolve, reject) => {
const id = setTimeout(resolve, ms);
signal.addEventListener("abort", () => {
clearTimeout(id);
reject(signal.reason ?? new DOMException("Aborted", "AbortError"));
});
});
}
async function main() {
// This signal cancels itself after 20ms
const signal = AbortSignal.timeout(20);
try {
await delay(1000, signal); // wants 1s, but the signal fires first
console.log("done");
} catch (err) {
console.log("timed out:", err.name);
// ✅ Expected output: timed out: TimeoutError
}
}
main();🎯 Your Turn #2 — Identify the Cancellation
Replace the blank so the code checks for a deliberate abort instead of treating it as a real error.
function makeAbortError() {
return new DOMException("Aborted", "AbortError");
}
try {
throw makeAbortError();
} catch (err) {
// 👉 replace ___ with the property that holds "AbortError"
if (err.___ === "AbortError") {
console.log("cancelled on purpose"); // ✅ Expected output: cancelled on purpose
} else {
console.log("real error");
}
}🧠 Quick Recall
Predict the output before revealing the answer.
true — abort() permanently flips the signal's aborted flag.
A DOMException with name === "AbortError".
No — the signal stays aborted. Create a fresh new AbortController() for each new operation.
Common Errors & Fixes
- 1️⃣ Passing the controller instead of its signal
- 2️⃣ Treating an abort as a real failure
- 3️⃣ Reusing one controller for every request
- 4️⃣ Forgetting to remove abort listeners
🎯 Mini Challenge — Cancel the Loser
Start two waits. When the fast one finishes, abort the slow one so its cleanup runs. This mirrors cancelling a stale request once fresher data arrives.
function delay(ms, signal, label) {
return new Promise((resolve, reject) => {
const id = setTimeout(() => resolve(label), ms);
signal.addEventListener("abort", () => {
clearTimeout(id);
console.log(label, "was cancelled");
reject(new DOMException("Aborted", "AbortError"));
});
});
}
async function main() {
const slow = new AbortController();
// Kick off the slow task (we may cancel it)
const slowPromise = delay(500, slow.signal, "slow").catch(() => {});
// The fast task wins
const winner = await delay(20, new AbortController().signal, "fast");
console.log("winner:", winner);
// ✅ Expected output: winner: fast
// No longer need the slow one -> cancel it
slow.abort();
await slowPromise;
// ✅ Expected output: slow was cancelled
}
main();Quick Reference — AbortController
| Goal | Code |
|---|---|
| Create | const c = new AbortController(); |
| Pass to fetch | fetch(url, { signal: c.signal }) |
| Cancel | c.abort(); |
| Detect | e.name === "AbortError" |
| Auto deadline | AbortSignal.timeout(3000) |
📚 Keep Learning
- • MDN — AbortController
- • MDN — AbortSignal (timeout, any)
- • MDN — fetch() with a signal
🎉 Lesson Complete — AbortController!
- ✅ A controller owns a one-shot signal you hand to async work
- ✅ abort() cancels everything listening; fetch rejects with an AbortError
- ✅ Check err.name === "AbortError" to ignore deliberate cancellations
- ✅ AbortSignal.timeout(ms) gives you a deadline with no controller
Practice quiz
What two things does a new AbortController() give you?
- A signal and an abort() method
- A promise and a resolve() method
- Two separate signals
- A signal and a timeout
Answer: A signal and an abort() method. A controller exposes a read-only signal you hand to async work, plus an abort() method that trips that signal.
After controller.abort(), what is controller.signal.aborted?
- false
- true
- undefined
- It throws
Answer: true. abort() permanently flips the signal's aborted flag to true.
When you abort a fetch, the fetch promise rejects with what?
- A plain Error
- A DOMException whose name is 'AbortError'
- null
- A TypeError
Answer: A DOMException whose name is 'AbortError'. An aborted fetch rejects with a DOMException whose name is 'AbortError'.
How do you tell a deliberate cancellation apart from a real network failure?
- Check err.message
- Check err.code
- Check err.name === 'AbortError'
- You cannot tell them apart
Answer: Check err.name === 'AbortError'. Checking err.name === 'AbortError' distinguishes an intentional abort from a genuine error.
Can you reuse an AbortController after calling abort()?
- Yes, it resets automatically
- No — the signal stays aborted forever
- Only if you call reset()
- Only for fetch, not timers
Answer: No — the signal stays aborted forever. A signal is one-shot; once aborted it stays aborted, so you create a fresh controller per operation.
What does AbortSignal.timeout(ms) return?
- A controller
- A promise
- A signal that auto-aborts after ms milliseconds
- A number
Answer: A signal that auto-aborts after ms milliseconds. AbortSignal.timeout(ms) returns a signal that aborts itself after the given delay, with no controller needed.
Which value do you pass to fetch's options?
- signal: controller
- signal: controller.signal
- controller: signal
- abort: controller
Answer: signal: controller.signal. You pass the signal, not the controller: fetch(url, { signal: controller.signal }).
When a signal created by AbortSignal.timeout fires, signal.reason is a DOMException named what?
- AbortError
- TimeoutError
- RangeError
- DeadlineError
Answer: TimeoutError. A timeout signal aborts with a DOMException whose name is 'TimeoutError'.
What event does a signal fire when it is aborted?
- 'cancel'
- 'stop'
- 'abort'
- 'error'
Answer: 'abort'. The signal fires an 'abort' event, which any listener attached to that signal reacts to.
What combines several signals so an operation aborts when ANY of them fires?
- AbortSignal.all([...])
- AbortSignal.any([...])
- AbortSignal.race([...])
- AbortSignal.merge([...])
Answer: AbortSignal.any([...]). AbortSignal.any([...]) aborts as soon as any of the supplied signals fires.
Continue this course
- Previous: Async Iterators & for-await-of
- Next: Web Workers (Background Threads) — Run CPU-heavy work off the main thread to keep the UI responsive
- Quick reference: JavaScript cheat sheet
Frequently asked questions
What is the relationship between AbortController and AbortSignal?
An AbortController is the remote control; its signal property is the receiver. You hand the signal to an async operation (like fetch) and keep the controller. Calling controller.abort() flips the signal's aborted flag to true and fires an 'abort' event, which any operation watching that signal reacts to. One controller can drive many operations that all share its single signal.
What happens to a fetch when its signal is aborted?
The fetch promise rejects with a DOMException whose name is 'AbortError'. You catch it like any rejection and can check err.name === 'AbortError' to tell a deliberate cancellation apart from a real network error. The browser also tears down the underlying request so the bytes stop coming over the wire.
Can I reuse an AbortController after calling abort()?
No — a signal is one-shot. Once aborted, it stays aborted forever, and any new operation you attach to that same signal aborts immediately. For each new cancellable operation (for example each new search keystroke), create a fresh AbortController so you get a clean, un-aborted signal.
What is AbortSignal.timeout?
AbortSignal.timeout(ms) is a shortcut that returns a signal which auto-aborts after the given number of milliseconds — no controller needed. It is perfect for putting a hard deadline on a fetch: pass it as the signal and the request cancels itself if it runs long. There is also AbortSignal.any([...]) to combine several signals into one.