Cancel fetch requests in React with AbortController, a browser API that stops an in-flight request. Create a controller, pass its signal to fetch, and call abort in the Effect cleanup. This stops the network work itself, not just the state update that follows it.
Create a controller and pass its signal
An AbortController links a request to a way to stop it. Pass the controller's signal into the fetch options, and the request becomes cancelable.
const controller = new AbortController();
fetch("/api/posts", { signal: controller.signal })
.then((res) => res.json())
.then((data) => setPosts(data));The signal is how the browser knows which request to cancel. Without it, a fetch promise has no way to be stopped from the outside, and the only option is to let it finish and ignore the result.
The signal also stops the reading of the response body, so aborting mid-download ends that too. You can reuse one controller for several requests, since the same signal aborts all of them. AbortController is a baseline browser API, available in all modern browsers and modern Node.js without a polyfill.
Abort in the Effect cleanup
Inside a component, the controller belongs in the Effect that starts the request. The cleanup aborts it, so a component that unmounts while the request is still running cancels that request instead of letting it finish in the background.
useEffect(() => {
const controller = new AbortController();
fetch("/api/posts", { signal: controller.signal })
.then((res) => res.json())
.then((data) => setPosts(data));
return () => controller.abort();
}, []);React runs the cleanup before the Effect re-runs and when the component unmounts. Aborting there ties the request's lifetime to the component's lifetime, which is what most data views want.
In development with Strict Mode, React runs the Effect twice, so the first cleanup aborts the first request right away. You may see two network entries even though only the second one matters, and that is expected development behavior, not a bug.
Cancelling also frees browser resources sooner than letting a response finish and discarding it. The basic fetch setup behind this example is in how to fetch API data in React.
Tell aborts apart from real errors
An aborted fetch rejects its promise with an AbortError. Without a check, your error state fires for what is really a normal cancel.
fetch("/api/posts", { signal: controller.signal })
.then((res) => res.json())
.then((data) => setPosts(data))
.catch((error) => {
if (error.name === "AbortError") return;
setStatus("error");
});The catch returns early for an abort and only sets the error state for a genuine failure. The AbortError is a DOMException, and the signal also exposes an aborted boolean you can read before doing more work.
Whichever check you use, the goal is the same. A cancel is not a failure, so it should not turn the screen into an error message.
Treating an abort as an error would show a broken UI every time the user navigates away mid-request. Checking for the abort early also keeps the rest of your error handling focused on problems that actually need a retry or a message.
Cancel from a button or a new request
Cleanup handles unmounts, but sometimes the user wants to stop a request explicitly, such as a cancel button on a search. Keep the controller in a ref so an event handler can reach the current request.
The ref survives re-renders, unlike a local variable that would be recreated each time. The same pattern works for a load button that replaces the current request with a new one.
const controllerRef = useRef(null);
function startSearch() {
controllerRef.current?.abort();
const controller = new AbortController();
controllerRef.current = controller;
fetch(`/api/search?q=${query}`, { signal: controller.signal })
.then((res) => res.json())
.then((data) => setResults(data));
}Each call aborts the previous request before starting a new one, so only the latest search can resolve. The optional chaining on controllerRef.current covers the first call, when no controller exists yet.
Combining this with the cleanup abort gives both explicit cancels and unmount cancels. When more async steps follow the fetch, keep an ignore flag too, the pattern behind build a useFetch hook with AbortController. A failed request also needs a path back to loading, which is covered in how to retry failed API requests in React.
Rune AI
Key Insights
- Create a controller and pass controller.signal to fetch.
- Call controller.abort in the Effect cleanup.
- Check error.name === "AbortError" to ignore intentional cancels.
- Abort from an event handler to cancel a request on user action.
- Keep an ignore flag when more async work follows the fetch.
Frequently Asked Questions
What does AbortController actually cancel?
Is AbortController available everywhere?
Should I use AbortController instead of an ignore flag?
Conclusion
Create an AbortController, pass its signal to fetch, and call abort in the Effect cleanup so an unmounting component stops its own request. Check for AbortError in the catch block so an intentional cancel does not look like a failure.
More in this topic
How to Build a Dropdown Menu in React
Build a React dropdown menu with the ARIA menu button pattern. Handle open and close, keyboard arrows, and clicks outside the menu.
How to Animate React Components with Motion
Animate React components with the Motion library. Set up motion, add enter, hover, and exit animations, and respect reduced motion.
Headless UI Components Explained: Logic Without Locked Styling
Understand headless UI components and how libraries like Radix give you unstyled, accessible behavior that you style yourself.