Cancelling In‑Flight Fetch Requests with AbortController

Modern web applications frequently make HTTP requests that users might not need to complete. A user typing in a search box generates multiple requests, but only the latest matters. A user navigating away from a page makes any pending requests for that page irrelevant. Without proper cancellation, these unnecessary requests waste bandwidth, consume server resources, and can cause stale data to appear in your UI.
The AbortController API provides a clean, standardized way to cancel fetch requests and other asynchronous operations. This article shows you how to implement request cancellation using AbortController, covering practical patterns like search debouncing, component cleanup, and timeout handling.
Key Takeaways
- AbortController creates a signal that fetch monitors for cancellation events
- Always check for AbortError in catch blocks to distinguish cancellations from failures
- Cancel previous requests in search interfaces to prevent race conditions
- Use cleanup functions in framework components to cancel requests on unmount
- Combine AbortController with setTimeout for request timeout functionality
- Each AbortController is single-use - create new ones for retry logic
- Node.js 18+ includes native AbortController support
What AbortController and AbortSignal Do
AbortController creates a controller object that manages cancellation through an associated AbortSignal. The controller has one job: calling abort()
when you want to cancel an operation. The signal acts as a communication channel that fetch and other APIs monitor for cancellation events.
const controller = new AbortController()
const signal = controller.signal
// The signal starts as not aborted
console.log(signal.aborted) // false
// Calling abort() changes the signal's state
controller.abort()
console.log(signal.aborted) // true
When you pass a signal to fetch and later call abort()
, the fetch promise rejects with a DOMException
named AbortError
. This lets you distinguish between cancellations and actual network failures.
Basic AbortController Implementation
To cancel a fetch request, create an AbortController, pass its signal to fetch, then call abort()
when needed:
const controller = new AbortController()
fetch('/api/data', { signal: controller.signal })
.then(response => response.json())
.then(data => console.log('Data received:', data))
.catch(error => {
if (error.name === 'AbortError') {
console.log('Request was cancelled')
} else {
console.error('Request failed:', error)
}
})
// Cancel the request
controller.abort()
The key points:
- Create the controller before making the request
- Pass
controller.signal
to the fetch options - Call
controller.abort()
to cancel - Check for
AbortError
in your catch block
How to Cancel Search Requests When Users Type
Search interfaces often trigger requests on every keystroke. Without cancellation, slow responses can arrive out of order, showing results for outdated queries. Here’s how to cancel previous search requests:
let searchController = null
function performSearch(query) {
// Cancel any existing search
if (searchController) {
searchController.abort()
}
// Create new controller for this search
searchController = new AbortController()
fetch(`/api/search?q=${encodeURIComponent(query)}`, {
signal: searchController.signal
})
.then(response => response.json())
.then(results => {
console.log('Search results:', results)
updateSearchUI(results)
})
.catch(error => {
if (error.name === 'AbortError') {
console.log('Search cancelled')
} else {
console.error('Search failed:', error)
showSearchError()
}
})
}
// Usage: each new search cancels the previous one
performSearch('javascript')
performSearch('javascript frameworks') // Cancels 'javascript' search
This pattern ensures only the most recent search results appear in your UI, preventing race conditions where slower requests overwrite faster ones.
Cancelling Requests on Component Unmount
In frontend frameworks, components often make fetch requests that should be cancelled when the component unmounts. This prevents memory leaks and errors from trying to update unmounted components.
React Example
import { useEffect, useState } from 'react'
function UserProfile({ userId }) {
const [user, setUser] = useState(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
const controller = new AbortController()
async function fetchUser() {
try {
const response = await fetch(`/api/users/${userId}`, {
signal: controller.signal
})
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
const userData = await response.json()
setUser(userData)
} catch (error) {
if (error.name !== 'AbortError') {
console.error('Failed to fetch user:', error)
}
} finally {
if (!controller.signal.aborted) {
setLoading(false)
}
}
}
fetchUser()
// Cleanup function cancels the request
return () => controller.abort()
}, [userId])
if (loading) return <div>Loading...</div>
return <div>{user?.name}</div>
}
Vue Example
export default {
data() {
return {
user: null,
controller: null
}
},
async mounted() {
this.controller = new AbortController()
try {
const response = await fetch(`/api/users/${this.userId}`, {
signal: this.controller.signal
})
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
this.user = await response.json()
} catch (error) {
if (error.name !== 'AbortError') {
console.error('Failed to fetch user:', error)
}
}
},
beforeUnmount() {
if (this.controller) {
this.controller.abort()
}
}
}
Implementing Request Timeouts with AbortController
Network requests can hang indefinitely. AbortController combined with setTimeout
provides a clean timeout mechanism:
function fetchWithTimeout(url, options = {}, timeoutMs = 5000) {
const controller = new AbortController()
const timeoutId = setTimeout(() => {
controller.abort()
}, timeoutMs)
return fetch(url, {
...options,
signal: controller.signal
}).finally(() => {
clearTimeout(timeoutId)
})
}
// Usage
fetchWithTimeout('/api/slow-endpoint', {}, 3000)
.then(response => response.json())
.then(data => console.log('Data:', data))
.catch(error => {
if (error.name === 'AbortError') {
console.log('Request timed out')
} else {
console.error('Request failed:', error)
}
})
For reusable timeout logic, create a utility function:
function createTimeoutSignal(timeoutMs) {
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), timeoutMs)
// Clean up timeout when signal is used
controller.signal.addEventListener('abort', () => {
clearTimeout(timeoutId)
}, { once: true })
return controller.signal
}
// Usage
fetch('/api/data', { signal: createTimeoutSignal(5000) })
.then(response => response.json())
.catch(error => {
if (error.name === 'AbortError') {
console.log('Request timed out or was cancelled')
}
})
Proper Error Handling for Cancelled Requests
Always distinguish between cancellations and genuine errors. AbortError indicates intentional cancellation, not a failure:
async function handleRequest(url) {
const controller = new AbortController()
try {
const response = await fetch(url, { signal: controller.signal })
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
return await response.json()
} catch (error) {
if (error.name === 'AbortError') {
// This is expected when we cancel - don't log as error
console.log('Request was cancelled')
return null
}
// This is an actual error that needs handling
console.error('Request failed:', error)
throw error
}
}
For applications with error tracking, exclude AbortError from error reports:
.catch(error => {
if (error.name === 'AbortError') {
// Don't report cancellations to error tracking
return
}
// Report actual errors
errorTracker.captureException(error)
throw error
})
Managing Multiple Requests
When dealing with multiple concurrent requests, you might need to cancel them all at once or manage them individually:
class RequestManager {
constructor() {
this.controllers = new Map()
}
async fetch(key, url, options = {}) {
// Cancel existing request with same key
this.cancel(key)
const controller = new AbortController()
this.controllers.set(key, controller)
try {
const response = await fetch(url, {
...options,
signal: controller.signal
})
return response
} finally {
this.controllers.delete(key)
}
}
cancel(key) {
const controller = this.controllers.get(key)
if (controller) {
controller.abort()
this.controllers.delete(key)
}
}
cancelAll() {
for (const controller of this.controllers.values()) {
controller.abort()
}
this.controllers.clear()
}
}
// Usage
const requestManager = new RequestManager()
// These requests can be managed independently
requestManager.fetch('user-profile', '/api/user/123')
requestManager.fetch('user-posts', '/api/user/123/posts')
// Cancel specific request
requestManager.cancel('user-profile')
// Cancel all pending requests
requestManager.cancelAll()
Browser Support and Node.js Compatibility
AbortController has excellent browser support:
- Chrome 66+
- Firefox 57+
- Safari 12.1+
- Edge 16+
For Node.js, AbortController is available natively in Node 18+. For earlier versions, use the abort-controller polyfill:
npm install abort-controller
// For Node.js < 18
const { AbortController } = require('abort-controller')
// Use normally
const controller = new AbortController()
Conclusion
AbortController provides a clean, standardized way to cancel fetch requests in modern web applications. The key patterns are: create a controller before each request, pass its signal to fetch, and call abort() when cancellation is needed. Always handle AbortError separately from genuine network failures to avoid treating intentional cancellations as errors.
The most common use cases—search request management, component cleanup, and timeout handling—follow straightforward patterns that you can adapt to your specific needs. With proper implementation, request cancellation improves both user experience and application performance by preventing unnecessary network activity and stale data updates.
Ready to implement request cancellation in your application? Start with the basic pattern for your most common use case, then expand to more complex scenarios as needed. The investment in proper request management pays dividends in application performance and user experience.
FAQs
No, once abort() is called, the controller's signal remains in the aborted state permanently. You must create a new AbortController for each new request or retry attempt.
No, abort() only prevents your JavaScript code from processing the response. If the request has already been sent over the network, the server may still receive and process it.
Create multiple controllers and store them in an array or Map, then call abort() on each one. Alternatively, use a single controller and pass its signal to multiple fetch calls - calling abort() once will cancel all of them.
Nothing happens. The abort() call is ignored if the request has already finished successfully or failed for other reasons.
Yes, AbortController works identically with both Promise chains and async/await. The fetch promise will reject with an AbortError when cancelled, which you can catch with try/catch blocks.