A user types three letters into a search field, then clears everything. Three requests have left, and the slowest may well answer last, overwriting the display with a stale result.
Waiting politely is not enough: you need a way to tell an operation in flight to stop. That is what AbortController is for.
Definition
An AbortController is an object exposing two things: a signal property, handed to the operation you want to watch, and an abort() method, which triggers the cancellation. The signal is the link between them.
const controller = new AbortController();
const signal = controller.signal;
console.log(signal.aborted); // false
signal.addEventListener("abort", () => {
console.log("canceled:", signal.reason.name); // canceled: AbortError
});
controller.abort();
console.log(signal.aborted); // trueCancellation is final: a triggered signal never goes back, and a fresh call needs a fresh controller.
Canceling a request
fetch() accepts the signal among its options. Once abort() has been called, the request is interrupted and the promise is rejected with an error named AbortError.
async function search(term, signal) {
try {
const response = await fetch("/api/search?q=" + term, { signal });
return await response.json();
} catch (err) {
if (err.name === "AbortError") return null; // deliberate cancellation
throw err; // real failure
}
}Telling the two apart is essential: a deliberate cancellation is not an incident and should neither reach the screen nor land in the error logs.
One signal, several uses
- Removing listeners in one move.
addEventListener("click", f, { signal })detaches the listener automatically on the firstabort(), however many there are. - Setting a time limit.
AbortSignal.timeout(5000)fires on its own after five seconds, with a reason namedTimeoutError, where a Promise.race() would leave the call running. - Combining reasons to stop.
AbortSignal.any([a, b])returns a signal triggered as soon as either of the two is. - Checking at the right moment.
signal.throwIfAborted()raises the error if cancellation has already happened, which lets a long loop bail out between two steps.
const controller = new AbortController();
const signal = AbortSignal.any([controller.signal, AbortSignal.timeout(5000)]);
// The request stops on whichever comes first: manual abort or five seconds
fetch("/api/report", { signal });abort() accepts a reason of your choosing: controller.abort(new Error("search replaced")). It shows up in signal.reason and in the error you receive, which makes logs far more readable than an anonymous AbortError.
Frequently asked questions
Can a controller be reused after cancellation?
No, a triggered signal stays triggered. Every cancelable operation needs its own controller, created as the work starts. In a search field you therefore keep the current controller, abort it on the next keystroke, and create a new one.
Is the request really stopped on the server?
The connection is cut, but the server may already have started, or even finished, its work. Cancellation protects the interface and frees the connection, it does not undo an effect already produced. A database write still has to be protected some other way.
Where does cancellation belong in a component-based interface?
In the cleanup of the effect that started the request, called when the component is removed or before a new call. That is what prevents updates against a screen that has gone, a reflex drilled in the React course.