How to Cancel Fetch Requests in React with AbortController

Cancel fetch requests in React with AbortController. Pass a signal to fetch, abort in the Effect cleanup, and tell aborts apart from real errors.

6 min read

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.

App.jsxApp.jsx
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.

App.jsxApp.jsx
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.

App.jsxApp.jsx
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.

App.jsxApp.jsx
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

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.
RunePowered by Rune AI

Frequently Asked Questions

What does AbortController actually cancel?

It aborts the network request and the reading of the response body. The fetch promise then rejects with an AbortError instead of resolving.

Is AbortController available everywhere?

Yes. It is a browser baseline API supported in all modern browsers, Web Workers, and modern Node.js, so you can use it without a polyfill.

Should I use AbortController instead of an ignore flag?

They solve related problems. AbortController stops the network work, while an ignore flag stops a late response from updating state. Use the abort in cleanup and keep the ignore flag when more async steps run after the fetch.

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.