How to Cancel a Fetch Request in JavaScript (2026)
June 29, 2026
To cancel a fetch request, create an AbortController, pass its signal to fetch(url, { signal }), and call controller.abort() when you want to stop. The request's promise rejects with an AbortError you can safely ignore. In React, call abort() from your useEffect cleanup.
TL;DR
Canceling a fetch comes down to one web-standard tool: AbortController. Create a controller, pass controller.signal to fetch, and call controller.abort() to stop the request — it then rejects with a DOMException named AbortError. For timeouts, skip the old setTimeout dance and use AbortSignal.timeout(ms), which rejects with a TimeoutError; combine a timeout with a cancel button using AbortSignal.any([...]). In React, create the controller inside useEffect and call abort() in the cleanup to kill stale requests and avoid race conditions. The core AbortController, timeout, and AbortSignal.any() behaviors shown here were verified against a live server on Node.js 22.
What you'll learn
- How to cancel a fetch request with
AbortControllerand asignal - How to detect and safely ignore the
AbortErrora cancel throws - How to cancel a fetch on unmount (and avoid race conditions) in a React
useEffect - Why your fetch fires twice in development, and when to worry about it
- How to add a request timeout with
AbortSignal.timeout() - How to combine a timeout and a cancel button with
AbortSignal.any() - Why an
AbortControlleris single-use, and how to reuse the pattern correctly - Which browsers and runtimes support these APIs
- How axios and TanStack Query handle cancellation
How do you cancel a fetch request in JavaScript?
Create an AbortController, pass its signal to fetch, and call abort() when you want to stop the request. AbortController is the standard, built-in way to make a fetch cancellable — no library required.1
// Create a controller and grab its signal
const controller = new AbortController();
fetch("/api/search?q=react", { signal: controller.signal })
.then((res) => res.json())
.then((data) => console.log(data))
.catch((err) => {
if (err.name === "AbortError") {
console.log("Request was cancelled");
} else {
throw err; // a real network or parsing error
}
});
// Later — cancel it
controller.abort();
When abort() is called, the fetch() promise rejects with a DOMException named AbortError.1 The signal is the read-only side of the controller that you hand to fetch; the controller keeps the abort() button. You can pass the same signal to several fetch calls, and a single abort() cancels all of them at once — useful when one user action kicks off a batch of requests.
How do you handle the AbortError after canceling a fetch?
Catch the rejection and check err.name === "AbortError". A cancel is something you triggered on purpose, so the usual move is to ignore that specific error and re-throw everything else. The async/await version reads cleanly:
const controller = new AbortController();
try {
const res = await fetch(url, { signal: controller.signal });
const data = await res.json();
// use data
} catch (err) {
if (err.name === "AbortError") return; // expected — do nothing
throw err; // anything else is a genuine failure
}
If you want to know why a request was cancelled, pass a reason to abort(). Whatever value you pass becomes the rejection reason and is also stored on signal.reason:
controller.abort(new Error("User navigated away"));
Calling controller.abort(new Error("User navigated away")) makes the fetch reject with that exact Error object — verified by identity in testing — instead of the default AbortError.1 You can also guard code that should not run once a signal is aborted with signal.throwIfAborted(), which throws the stored reason if the signal has already aborted and does nothing otherwise.1
How do you cancel a fetch request in React with useEffect?
Create the AbortController inside the effect, pass its signal to fetch, and return a cleanup function that calls controller.abort(). React runs that cleanup when the component unmounts and before the effect re-runs, so a stale request is cancelled before a new one starts.2
import { useEffect, useState } from "react";
function SearchResults({ query }) {
const [data, setData] = useState(null);
useEffect(() => {
const controller = new AbortController();
async function load() {
try {
const res = await fetch(`/api/search?q=${query}`, {
signal: controller.signal,
});
setData(await res.json());
} catch (err) {
if (err.name !== "AbortError") throw err;
}
}
load();
return () => controller.abort(); // cancel on unmount or when query changes
}, [query]);
return <pre>{JSON.stringify(data, null, 2)}</pre>;
}
This solves two real bugs. The first is a race condition: if query changes from "alice" to "bob", the slower "alice" response could land last and overwrite "bob". Aborting the old request makes that impossible. The second is the classic "can't update state on an unmounted component" pattern, because the cancelled request never reaches setData.
The React docs offer a second pattern — an ignore flag — that you can use instead of, or alongside, aborting:2
useEffect(() => {
let ignore = false;
fetch(`/api/search?q=${query}`)
.then((res) => res.json())
.then((data) => {
if (!ignore) setData(data);
});
return () => {
ignore = true;
};
}, [query]);
The difference is concrete: the ignore flag discards a stale response but the network request still completes, while AbortController actually cancels the request and saves the bandwidth. Aborting is the stronger choice when responses are large or expensive.
Why does my fetch run twice or abort immediately in React?
Because of <StrictMode> in development. React intentionally double-invokes effects in development — setup, then cleanup, then setup again — to help you catch missing cleanup.3 With an AbortController cleanup in place, that means your first request is aborted almost immediately, so you may see an extra request and an AbortError (or a "canceled" entry in the Network tab) while developing.
This is expected and development-only. In production the effect runs once and nothing is aborted prematurely. The fact that you see the cancel proves your cleanup works — it is not a bug to "fix" by deleting StrictMode or the cleanup function. Because the example above already ignores AbortError, the doubled request causes no visible problem for users.
How do you add a timeout to a fetch request?
Pass AbortSignal.timeout(ms) as the signal. The Fetch standard has no built-in timeout option, so this static method is the modern, official way to give a request a deadline.4 The signal aborts automatically after the given number of milliseconds and rejects the fetch with a TimeoutError DOMException.5
try {
const res = await fetch(url, { signal: AbortSignal.timeout(5000) });
const data = await res.json();
// use data
} catch (err) {
if (err.name === "TimeoutError") {
console.error("Request timed out after 5 seconds");
} else if (err.name === "AbortError") {
console.error("Request was cancelled by the user");
} else {
throw err; // a network or other error
}
}
The key detail is the error name. A timeout rejects with TimeoutError, while a user-triggered abort rejects with AbortError, so a single catch can tell the two apart — verified by running a 50 ms timeout against a one-second endpoint.5 That distinction matters: a timeout usually deserves a "try again" message, while a deliberate cancel usually deserves silence. (One historical caveat: Chromium-based browsers before Chrome 124 reported a timed-out fetch as an AbortError rather than a TimeoutError; current browsers follow the spec.6) This replaces the older pattern of creating an AbortController, starting a setTimeout that calls abort(), and clearing the timer by hand.
How do you combine a timeout with a cancel button?
Use AbortSignal.any() to merge several signals into one. It takes an iterable of signals and returns a signal that aborts as soon as any of them aborts — perfect for "cancel after 10 seconds or when the user clicks Cancel."7
const controller = new AbortController();
cancelButton.addEventListener("click", () => controller.abort());
const res = await fetch(url, {
signal: AbortSignal.any([controller.signal, AbortSignal.timeout(10_000)]),
});
The combined signal adopts the reason of whichever source signal aborts first.7 One caveat is worth knowing: MDN notes that, unlike a bare AbortSignal.timeout(), you cannot always rely on the error name alone to tell whether a combined abort came from the timeout.1 When you need the cause for certain, check which controller fired instead of the error name:
} catch (err) {
if (controller.signal.aborted) {
console.log("Cancelled by the user");
} else {
console.log("Request timed out");
}
}
controller.signal.aborted is true only when your own cancel handler ran, so it separates a manual cancel from a timeout reliably across engines. Either way, you get layered cancellation — timeout and manual — from a single signal, without juggling timers yourself.
Can you reuse an AbortController for multiple requests?
No — an AbortController (and its signal) is single-use. As MDN puts it, a signal "can only be used once"; after it aborts, any fetch using that same signal is rejected immediately.1 In testing, handing an already-aborted signal to a fresh fetch rejected it instantly with AbortError.
So the rule is: create a new AbortController for each logical operation you might cancel. What a single controller can do is cover several requests you want to cancel together — pass one signal to multiple fetch calls and a single abort() stops them all. If you need to cancel them independently, give each its own controller.
// One controller per request you might cancel on its own
function startRequest(url) {
const controller = new AbortController();
const promise = fetch(url, { signal: controller.signal });
return { promise, cancel: () => controller.abort() };
}
const { promise, cancel } = startRequest("/api/report");
// cancel(); // stops just this request
Is AbortController supported in all browsers and Node.js?
AbortController, AbortSignal, and abortable fetch are Baseline "widely available" features; MDN lists AbortController as available across browsers since March 2019, so the core cancel pattern is safe to use everywhere today.8 The two newer helpers are more recent: MDN lists AbortSignal.timeout() as Baseline 2024 (available since April 2024) and AbortSignal.any() as Baseline 2024 (available since March 2024).57
If you must support older browsers that lack AbortSignal.timeout(), the classic fallback still works: create an AbortController, start a setTimeout that calls controller.abort(), and clear the timer once the request resolves. These APIs also exist in modern server runtimes — Node.js, Deno, and Bun — so the same cancellation code runs on the server.
Do axios and TanStack Query cancel requests the same way?
Yes — both build on the same AbortController standard, so the concept carries over directly. With axios, you pass an AbortController signal in the request config; its legacy CancelToken API was deprecated back in v0.22.0 (October 2021) and you should prefer the signal approach.9
const controller = new AbortController();
axios.get("/api/search", { signal: controller.signal });
controller.abort();
With TanStack Query, you usually do not create a controller at all: the library passes an AbortSignal into your query function and aborts it automatically when a query becomes stale or inactive. You just forward that signal to fetch.10
useQuery({
queryKey: ["todos"],
queryFn: async ({ signal }) => {
const res = await fetch("/api/todos", { signal });
return res.json();
},
});
Bottom line
Canceling a fetch is a one-tool job: AbortController for manual cancellation, AbortSignal.timeout() for deadlines, and AbortSignal.any() to combine them — all standard, all built in, all supported across modern browsers and runtimes. In React, the whole pattern collapses to "make a controller in useEffect, abort it in the cleanup," which kills stale requests and the race conditions that come with them.
To go deeper on the async foundations underneath all of this, see how callbacks, promises, and async/await work and our guide to modern JavaScript async patterns. And if you would rather let a library handle cancellation, caching, and race conditions for you, see how TanStack Query manages requests and optimistic updates.
Footnotes
-
MDN Web Docs — AbortSignal interface (abort behavior, single-use signals,
reason,throwIfAborted). https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 -
React — Synchronizing with Effects (fetch cleanup, ignore vs. abort, race conditions). https://react.dev/learn/synchronizing-with-effects ↩ ↩2 ↩3
-
React —
<StrictMode>(re-runs Effects an extra time in development). https://react.dev/reference/react/StrictMode ↩ ↩2 -
WHATWG Fetch Standard — "Add a
timeoutoption, to prevent hanging" (issue #951; fetch has no built-in timeout option). https://github.com/whatwg/fetch/issues/951 ↩ ↩2 -
MDN Web Docs — AbortSignal: timeout() static method (TimeoutError on timeout, Baseline 2024). https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/timeout_static ↩ ↩2 ↩3 ↩4
-
mdn/browser-compat-data issue #20381 —
AbortSignal.timeout()surfacedAbortErrorinstead ofTimeoutErrorin Chromium before Chrome 124. https://github.com/mdn/browser-compat-data/issues/20381 ↩ -
MDN Web Docs — AbortSignal: any() static method (combine signals, first-reason wins, Baseline 2024). https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/any_static ↩ ↩2 ↩3 ↩4
-
MDN Web Docs — AbortController interface (Baseline "widely available," across browsers since March 2019). https://developer.mozilla.org/en-US/docs/Web/API/AbortController ↩
-
Axios — Cancellation (AbortController
signal;CancelTokendeprecated since v0.22.0). https://axios-http.com/docs/cancellation ↩ -
TanStack Query — Query Cancellation (
AbortSignalpassed to the query function). https://tanstack.com/query/latest/docs/framework/react/guides/query-cancellation ↩