Sometimes you want to stop a request: the user left the page, typed a new search, or it is simply taking too long. AbortController does that.
const controller = new AbortController();
fetch("/api/slow", { signal: controller.signal })
.then(response => response.json())
.catch(error => {
if (error.name === "AbortError") {
console.log("Request was cancelled");
} else {
console.log("Real error", error);
}
});
controller.abort(); // cancel it
How it works
- Create an
AbortController. - Pass its
signalin thefetchoptions. - Call
controller.abort(). The request stops, and the promise rejects with an error whosenameis"AbortError".
You should treat that error as expected, not as a failure to show to the user.
Try the mechanism
const controller = new AbortController();
controller.signal.addEventListener("abort", () => console.log("abort event fired"));
console.log(controller.signal.aborted); // → false
controller.abort();
console.log(controller.signal.aborted); // → true
A timeout
The old pattern uses a timer:
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 5000);
try {
const response = await fetch(url, { signal: controller.signal });
clearTimeout(timer);
// ...
} catch (error) {
// AbortError after 5 seconds
}
Modern browsers have a shortcut, AbortSignal.timeout():
try {
const response = await fetch(url, { signal: AbortSignal.timeout(5000) });
} catch (error) {
if (error.name === "TimeoutError") {
console.log("The server took too long.");
}
}
(A timeout from AbortSignal.timeout rejects with a TimeoutError, while a manual abort() gives an AbortError.) Check browser support if you must support older browsers, and fall back to the timer version.
Search-as-you-type: ignore stale requests
When a user types quickly, an older, slower request could finish after a newer one and show the wrong results. Cancel the previous request when starting a new one.
let controller;
async function search(term) {
controller?.abort(); // cancel the previous request
controller = new AbortController();
try {
const response = await fetch("/api/search?q=" + encodeURIComponent(term), { signal: controller.signal });
const results = await response.json();
render(results);
} catch (error) {
if (error.name !== "AbortError") showError(error);
}
}
Aborting other things
The same signal works for event listeners (addEventListener(type, fn, { signal })) and many other APIs, so one abort() can clean up several things at once.
Race does not cancel
Promise.race for timeouts leaves the request running. AbortController actually stops it. See Promise.race.