AbortController: Cancelling Fetch Requests and Async Operations
Here's a bug that exists in most React applications: a component fires a fetch request, unmounts before the response arrives, and then the response completes and tries to update state on an unmounted component. In the best case, React logs a warning. In the worst case, you get a race condition where a slow request from a previous user interaction resolves after a fast one, overwriting newer data with stale results.
AbortController is the solution. It lets you cancel in-flight fetch requests — and any other async operation you wire it to.
The API
AbortController is available in all modern browsers and Node.js 15+:
const controller = new AbortController()
const { signal } = controller
// Pass the signal to fetch
fetch('/api/search?q=react', { signal })
.then((res) => res.json())
.then((data) => console.log(data))
.catch((err) => {
if (err.name === 'AbortError') {
console.log('Request was cancelled')
} else {
throw err // rethrow real errors
}
})
// Cancel the request
controller.abort()
controller.abort() cancels the fetch and rejects its promise with an AbortError. The connection is closed immediately, even if the response was partially received.
In React: Cleanup in useEffect
The pattern for cancelling requests on unmount or dependency change is to create a controller in the effect and abort in the cleanup function:
useEffect(() => {
const controller = new AbortController()
async function fetchData() {
try {
const res = await fetch(`/api/search?q=${query}`, {
signal: controller.signal,
})
const data = await res.json()
setResults(data)
} catch (err) {
if (err.name !== 'AbortError') {
setError(err)
}
}
}
fetchData()
return () => controller.abort() // cleanup: cancel on unmount or query change
}, [query])
When query changes before the previous request completes, React runs the cleanup function (aborting the old request) before starting the new effect. When the component unmounts, same thing. The AbortError is caught and ignored.
Multiple Requests, One Controller
One controller can cancel multiple requests simultaneously:
const controller = new AbortController()
Promise.all([
fetch('/api/users', { signal: controller.signal }),
fetch('/api/posts', { signal: controller.signal }),
fetch('/api/comments', { signal: controller.signal }),
])
// Cancel all three at once
controller.abort()
This is useful when navigating away from a page that had several in-flight requests — one controller per "operation" rather than one per request.
Passing an Abort Reason
Since Chrome 98 and Node 17.3, you can pass a reason to abort():
controller.abort(new Error('User navigated away'))
The reason is available on signal.reason and as the AbortError's cause:
try {
await fetch(url, { signal })
} catch (err) {
if (err.name === 'AbortError') {
console.log('Cancelled because:', err.cause) // 'User navigated away'
}
}
Using the Signal Without Fetch
AbortSignal isn't limited to fetch. You can use it with any async operation by checking signal.aborted or listening to the abort event:
async function processItems(items, signal) {
for (const item of items) {
if (signal.aborted) throw new DOMException('Aborted', 'AbortError')
await processItem(item)
}
}
// Or with addEventListener
signal.addEventListener('abort', () => {
cleanup()
})
This lets you build cancellable higher-level async functions that compose well with the standard signal pattern.
AbortSignal.timeout()
For requests that should be automatically cancelled after a time limit, AbortSignal.timeout() creates a signal that aborts after a given number of milliseconds — no need to set up a controller manually:
fetch('/api/data', {
signal: AbortSignal.timeout(5000), // cancel after 5 seconds
})
For combining a timeout with manual cancellation:
const controller = new AbortController()
const timeoutSignal = AbortSignal.timeout(5000)
const combinedSignal = AbortSignal.any([controller.signal, timeoutSignal])
fetch('/api/data', { signal: combinedSignal })
AbortSignal.any() creates a signal that aborts when any of the provided signals abort.
In Data Fetching Libraries
React Query, SWR, and TanStack Query all wire up AbortController internally. When a query becomes stale or the component unmounts, they abort the in-flight request automatically. If you're using one of these libraries, you get cancellation for free — but understanding AbortController helps you handle edge cases and build custom hooks correctly.
Conclusion
AbortController is the standard mechanism for cancelling async operations in the browser. Use it in every useEffect that fires a fetch request: create a controller in the effect, pass signal to fetch, and call controller.abort() in the cleanup. This eliminates unmount warnings, prevents race conditions from stale responses, and frees up network resources immediately when the result is no longer needed.
