Runnable lesson · press Try it on the examples to edit and run them here.
Sometimes a request is no longer needed: the user typed another letter, navigated away or closed the dialog. You can cancel it so it stops using bandwidth and cannot update state later.
AbortController in two lines
const controller = new AbortController();
fetch(url, { signal: controller.signal }); // give fetch the signal
controller.abort(); // later: cancel the request
When you abort, the fetch promise rejects with an error whose name is "AbortError". This is not a failure to report to the user; it is what you asked for. Read more in Abort Fetch.
Abort in an effect cleanup
The cleanup function is the natural place. It runs when the dependencies change or the component unmounts, which is exactly when the old request becomes unwanted:
function slowApi(text, signal) {
// A pretend request that honours the AbortSignal, like fetch does
return new Promise((resolve, reject) => {
const id = setTimeout(() => resolve("Loaded: " + text), 500);
signal.addEventListener("abort", () => {
clearTimeout(id);
reject(new DOMException("Aborted", "AbortError"));
});
});
}
function App() {
const [query, setQuery] = React.useState("");
const [result, setResult] = React.useState("");
const [aborted, setAborted] = React.useState(0);
React.useEffect(() => {
if (!query) return;
const controller = new AbortController();
slowApi(query, controller.signal)
.then(setResult)
.catch((err) => {
if (err.name === "AbortError") { setAborted((n) => n + 1); return; } // expected: ignore
setResult("Error: " + err.message);
});
return () => controller.abort(); // cancel the previous request
}, [query]);
return (
<div>
<label>Search <input aria-label="Search" value={query} onChange={(e) => setQuery(e.target.value)} /></label>
<p>{result}</p>
<small>Requests cancelled: {aborted}</small>
</div>
);
}
Type quickly: each new letter cancels the previous request, and the counter goes up. Only the final request completes. (In a real project the first argument of fetch replaces slowApi: fetch(url, { signal: controller.signal }).)
With a real fetch
const url = "data:application/json,[1,2,3]";
function App() {
const [text, setText] = React.useState("Loading...");
React.useEffect(() => {
const controller = new AbortController();
async function load() {
try {
const response = await fetch(url, { signal: controller.signal });
if (!response.ok) throw new Error("HTTP " + response.status);
const data = await response.json();
setText("Got " + data.length + " items");
} catch (err) {
if (err.name !== "AbortError") setText("Failed: " + err.message);
}
}
load();
return () => controller.abort();
}, []);
return (
<div>
<p>Data</p>
<p>{text}</p>
</div>
);
}
Remember: the AbortError branch must not set an error state. Otherwise a normal cancel looks like a failure.
Timeouts
AbortSignal.timeout(ms) creates a signal that aborts after a delay, with no controller:
const response = await fetch(url, { signal: AbortSignal.timeout(8000) });
On timeout the error name is "TimeoutError". To combine a timeout with your own cancel signal, use AbortSignal.any([controller.signal, AbortSignal.timeout(8000)]) in browsers that support it.
Strict Mode in development
In development, Strict Mode runs effects twice. With the pattern above you will see the first request aborted straight away and the second one finish. That is correct behaviour. In the Network tab the first request shows as cancelled.
Abort vs ignore flag
| Technique | Stops the network request | Prevents the late state update |
|---|---|---|
ignore flag in cleanup | No (the request still finishes) | Yes |
AbortController | Yes (browser cancels it) | Yes, because the promise rejects |
Aborting saves bandwidth and server work; the ignore flag is enough for small data. Aborting does not cancel what the server has already started, so do not rely on it for actions that must not happen (such as payments).
Cancelling uploads and other things
AbortController also works with other APIs that accept a signal: event listeners (addEventListener(..., { signal })), streams and many libraries (Axios and TanStack Query support it).
Key points
- Create an
AbortControllerin the effect, pass itssignaltofetch, and callabort()in cleanup. - Treat
AbortErroras expected: do not show it as an error. AbortSignal.timeout(ms)adds a timeout.