Cancel Fetch Requests with AbortController

Pass an abort signal into fetch
In browser JavaScript, fetch() does not return a cancel method. Cancellation travels through an AbortSignal, usually created by an AbortController.
const controller = new AbortController();
const responsePromise = fetch("/api/search?q=otter", {
signal: controller.signal,
});
controller.abort();
Calling abort() marks the signal as aborted and causes the fetch operation to reject if it has not already completed. According to MDN's AbortController reference, cancellation can also interrupt consumption of a response body or stream.
One controller may signal several operations, but it is single-use: once aborted, its signal stays aborted. Create a fresh controller for a new request. Reusing the old one produces an immediate rejection, which is correct behavior and a surprisingly efficient way to make a search box appear haunted.
Build a cancelable JSON helper
Let the caller own cancellation by accepting a signal:
async function fetchJson(url, { signal } = {}) {
const response = await fetch(url, { signal });
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
}
fetch() resolves after an HTTP response even when its status is 404 or 500, so check response.ok or the status explicitly. Network failure, cancellation, HTTP failure, and invalid JSON are different outcomes. Do not turn all of them into “No results.”
The Fetch Standard defines the request's associated abort signal and rejection behavior. Application code still decides how to present that outcome.
Connect cancellation to interface state
This example cancels an earlier search before starting the next one:
let activeController = null;
async function runSearch(query) {
activeController?.abort("Superseded by a newer search");
activeController = new AbortController();
const controller = activeController;
try {
const data = await fetchJson(
`/api/search?q=${encodeURIComponent(query)}`,
{ signal: controller.signal },
);
if (activeController === controller) {
renderResults(data);
}
} catch (error) {
if (controller.signal.aborted) {
return;
}
showError(error);
} finally {
if (activeController === controller) {
activeController = null;
}
}
}
The identity check prevents an older request's cleanup from clearing the controller for a newer request. It also prevents stale results from replacing fresh ones if completion and user input race.
The string passed to abort() becomes the signal's reason in supporting implementations. Without a custom reason, cancellation commonly rejects with a DOMException named AbortError. Checking controller.signal.aborted is appropriate here because this code owns that exact signal. Do not classify every caught TypeError as cancellation; fetch uses TypeError for other failures too.
Browse Web APIs for patterns that keep interface state tied to the current operation.
Add a timeout without losing cleanup
A timeout is one policy for triggering the same controller:
async function fetchJsonWithTimeout(url, milliseconds) {
const controller = new AbortController();
const timer = setTimeout(() => {
controller.abort(new Error("Request timed out"));
}, milliseconds);
try {
return await fetchJson(url, { signal: controller.signal });
} finally {
clearTimeout(timer);
}
}
Validate milliseconds before calling this helper; it should be a finite, non-negative number appropriate to the application. Clearing the timer in finally matters after success and every failure. Otherwise, a completed request leaves a callback waiting around to abort a controller nobody needs.
Modern runtimes also provide helpers such as AbortSignal.timeout() and signal composition in some environments. Check the actual browser or runtime support required by your project before replacing an explicit controller. A tutorial's “modern” and your embedded webview's “modern” may not have met.
Abort on component cleanup
When a view, component, or route starts a request, abort it during that view's cleanup. The framework-specific hook differs, but the ownership rule stays simple: the scope that starts the operation should arrange to stop or ignore it when that scope ends.
Aborting prevents your client from continuing to wait or consume a body. It does not guarantee that the server stopped processing, and it cannot reverse a database update or payment already accepted. Use idempotency, server-side cancellation, transaction design, or application-specific safeguards for consequential operations.
Test all four exits
Test successful JSON, an HTTP error, invalid JSON, and cancellation. Also start request A, immediately start request B, and confirm A cannot overwrite B. Use developer tools to slow the network rather than hoping your connection has an introspective afternoon.
Our optional-chaining guide explains the cleanup call activeController?.abort() and when that convenience would hide a missing requirement. Continue through language tools when the control flow is correct but the syntax still deserves inspection.