The caller owns a cancellation signal
new AbortController() creates a controller with a signal property. Pass the signal to an operation such as fetch using { signal: controller.signal }. Calling abort asks participating work to stop. It is a request, not a magic interruption of arbitrary JavaScript. A custom operation must cooperate with the signal.
Check signal.aborted when starting, because the signal may already be cancelled. For future cancellation, listen for its abort event. Remove listeners and clear timers when settling so completed work does not retain unnecessary callbacks. A controller’s signal cannot be reset; use a new controller for a new operation.
Browser fetch normally rejects with an AbortError when aborted without a custom reason. The small function below models that behavior with an Error whose name is set explicitly. Your UI should treat intentional cancellation differently from “service unavailable.” Ordinary failures should remain visible.
Cancellation and correctness are related but separate. A service might ignore the signal or finish just before abort. On Day 5 we will also prevent stale results from replacing a newer search. Cancelling saves work when possible; a current-request check protects the displayed result.
Predict and trace
Write the output before running. Follow the values through each call; if your prediction differs, identify the first step that surprised you.
async function main() {
function abortError() {
const error = new Error("Cancelled");
error.name = "AbortError";
return error;
}
async function read(signal) {
if (signal.aborted) {
throw abortError();
}
return "Ready";
}
const controller = new AbortController();
controller.abort();
try {
await read(controller.signal);
} catch (error) {
console.log(error.name);
}
console.log(controller.signal.aborted);
}
main();Check your prediction
AbortError
trueThis example cancels before work starts. The final demo service also listens for abort while its timer is pending.
Cancel work that is already pending
The earlier async read settles immediately. Now keep a promise pending with a timer and cancel it after starting. The signal emits an abort event. The same onAbort function is registered and removed; creating a different callback would not remove the original listener. No timing guess is needed: abort happens synchronously before a timer task can run.
async function main() {
function waitForTitle(signal) {
return new Promise((resolve, reject) => {
function onAbort() {
clearTimeout(timer);
signal.removeEventListener("abort", onAbort);
const error = new Error("Cancelled");
error.name = "AbortError";
reject(error);
}
const timer = setTimeout(() => {
signal.removeEventListener("abort", onAbort);
resolve("Notes");
}, 0);
if (signal.aborted) {
onAbort();
} else {
signal.addEventListener("abort", onAbort, { once: true });
}
});
}
const controller = new AbortController();
const pending = waitForTitle(controller.signal);
controller.abort();
try {
console.log(await pending);
} catch (error) {
console.log(error.name);
}
}
main();Check your prediction
AbortErrorThe { once: true } listener option removes the listener after its first event. Success also removes it, and cancellation clears the timer. The timer variable is assigned before onAbort can run; declaring a function does not execute its body.
Build it yourself
AbortController & Cancellation: practise
Write read(signal) which rejects an already-aborted signal with Error(“Stopped”) and otherwise returns “Ready” asynchronously. Print the result with a fresh signal and catch the aborted case.
// The two attempts need separate controllers.Hint 1: the idea
Inspect the signal, not an unrelated boolean.
Hint 2: a first step
An async throw becomes a rejected promise that the caller awaits.
Show one solution
async function main() {
async function read(signal) {
if (signal.aborted) {
throw new Error("Stopped");
}
return "Ready";
}
const first = new AbortController();
console.log(await read(first.signal));
const second = new AbortController();
second.abort();
try {
await read(second.signal);
} catch (error) {
console.log(error.message);
}
}
main();Ready
StoppedCall abort twice and observe that the signal stays aborted. Explain why reusing that signal for a retry would immediately reject.
Test a changed requirement
Call abort twice and observe that the signal stays aborted. Explain why reusing that signal for a retry would immediately reject.
Using the pending example, remove only the caller’s abort line and rerun: it should fulfill with Notes. Then create a fresh controller, abort before calling waitForTitle, and verify rejection still occurs. Those are three different schedules, not three different validation rules.
Explain before moving on
Why might a current-request check still be needed after abort? Who decides that cancellation is intentional?
Going further
See MDN’s AbortSignal reference for the signal API. The exercises use explicit controllers and listeners so the ownership and cleanup are visible.
Completion is saved in this browser only. It does not measure mastery.