Docs
README
16.7 AbortController & Cancellation
Overview
AbortController provides a standard way to cancel asynchronous operations in JavaScript. It's essential for managing long-running requests, preventing memory leaks, and improving user experience by allowing users to cancel operations they no longer need.
Learning Objectives
By the end of this section, you will:
- •Understand the AbortController and AbortSignal APIs
- •Cancel fetch requests and other async operations
- •Implement timeout patterns with AbortController
- •Handle abort events and cleanup properly
- •Create cancellable custom async operations
- •Combine multiple abort signals
Prerequisites
- •Understanding of Promises and async/await
- •Familiarity with the Fetch API
- •Basic knowledge of event handling
1. AbortController Basics
Creating and Using AbortController
// Create an AbortController
const controller = new AbortController();
const signal = controller.signal;
// Pass signal to fetch
fetch('/api/data', { signal })
.then((response) => response.json())
.then((data) => console.log(data))
.catch((error) => {
if (error.name === 'AbortError') {
console.log('Fetch was cancelled');
} else {
console.error('Fetch error:', error);
}
});
// Cancel the request
controller.abort();
AbortSignal Properties and Events
const controller = new AbortController();
const signal = controller.signal;
// Check if already aborted
console.log(signal.aborted); // false
// Listen for abort event
signal.addEventListener('abort', () => {
console.log('Operation was aborted');
console.log('Reason:', signal.reason);
});
// Abort with a custom reason
controller.abort('User cancelled the operation');
console.log(signal.aborted); // true
console.log(signal.reason); // 'User cancelled the operation'
2. Cancelling Fetch Requests
Basic Fetch Cancellation
async function fetchWithCancel(url) {
const controller = new AbortController();
const fetchPromise = fetch(url, {
signal: controller.signal,
});
// Return both the promise and cancel function
return {
promise: fetchPromise.then((r) => r.json()),
cancel: () => controller.abort(),
};
}
// Usage
const { promise, cancel } = fetchWithCancel('/api/large-data');
// Cancel after 5 seconds if not complete
setTimeout(cancel, 5000);
try {
const data = await promise;
console.log(data);
} catch (error) {
if (error.name === 'AbortError') {
console.log('Request was cancelled');
}
}
Cancel on Component Unmount (React Pattern)
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
async function fetchData() {
try {
setLoading(true);
const response = await fetch(url, {
signal: controller.signal,
});
const result = await response.json();
setData(result);
setError(null);
} catch (err) {
if (err.name !== 'AbortError') {
setError(err);
}
} finally {
setLoading(false);
}
}
fetchData();
// Cleanup: cancel on unmount
return () => controller.abort();
}, [url]);
return { data, loading, error };
}
3. Timeout Patterns
Using AbortSignal.timeout()
// Modern approach (ES2022+)
try {
const response = await fetch('/api/data', {
signal: AbortSignal.timeout(5000), // 5 second timeout
});
const data = await response.json();
} catch (error) {
if (error.name === 'TimeoutError') {
console.log('Request timed out');
} else if (error.name === 'AbortError') {
console.log('Request was aborted');
}
}
Custom Timeout Implementation
function fetchWithTimeout(url, options = {}, timeout = 5000) {
const controller = new AbortController();
const signal = controller.signal;
// Set up timeout
const timeoutId = setTimeout(() => {
controller.abort('Request timeout');
}, timeout);
return fetch(url, { ...options, signal }).finally(() =>
clearTimeout(timeoutId)
);
}
// Usage
try {
const response = await fetchWithTimeout('/api/slow-endpoint', {}, 3000);
const data = await response.json();
} catch (error) {
console.log(error.name, error.message);
}
Combining Timeout with Manual Abort
function createCancellableFetch(url, timeout = 10000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => {
controller.abort('Timeout exceeded');
}, timeout);
const promise = fetch(url, { signal: controller.signal })
.then((response) => {
clearTimeout(timeoutId);
return response.json();
})
.catch((error) => {
clearTimeout(timeoutId);
throw error;
});
return {
promise,
cancel: (reason = 'Cancelled by user') => {
clearTimeout(timeoutId);
controller.abort(reason);
},
};
}
4. Combining Multiple Signals
AbortSignal.any() (ES2024)
// Cancel if either timeout OR manual abort
const controller = new AbortController();
const combinedSignal = AbortSignal.any([
controller.signal,
AbortSignal.timeout(5000),
]);
fetch('/api/data', { signal: combinedSignal })
.then((r) => r.json())
.then((data) => console.log(data))
.catch((error) => {
console.log('Aborted:', error.message);
});
// Can still manually abort before timeout
controller.abort('User cancelled');
Polyfill for AbortSignal.any()
function combineSignals(...signals) {
const controller = new AbortController();
function onAbort() {
controller.abort(this.reason);
cleanup();
}
function cleanup() {
signals.forEach((signal) => {
signal.removeEventListener('abort', onAbort);
});
}
signals.forEach((signal) => {
if (signal.aborted) {
controller.abort(signal.reason);
} else {
signal.addEventListener('abort', onAbort);
}
});
return controller.signal;
}
// Usage
const userController = new AbortController();
const timeoutSignal = AbortSignal.timeout(5000);
const combined = combineSignals(userController.signal, timeoutSignal);
5. Cancellable Custom Operations
Making Any Async Operation Cancellable
function cancellable(asyncFn) {
return function (...args) {
const controller = new AbortController();
const promise = new Promise(async (resolve, reject) => {
controller.signal.addEventListener('abort', () => {
reject(
new DOMException(controller.signal.reason || 'Aborted', 'AbortError')
);
});
try {
const result = await asyncFn(...args, controller.signal);
resolve(result);
} catch (error) {
reject(error);
}
});
return {
promise,
cancel: (reason) => controller.abort(reason),
};
};
}
// Usage
const cancellableOperation = cancellable(async (data, signal) => {
// Check if aborted before starting
if (signal.aborted) {
throw new DOMException('Aborted', 'AbortError');
}
// Simulate long operation with checkpoints
await step1(data);
if (signal.aborted) throw new DOMException('Aborted', 'AbortError');
await step2(data);
if (signal.aborted) throw new DOMException('Aborted', 'AbortError');
return await step3(data);
});
const { promise, cancel } = cancellableOperation(myData);
Cancellable Promise Utility
class CancellablePromise {
constructor(executor) {
this.controller = new AbortController();
this.promise = new Promise((resolve, reject) => {
this.controller.signal.addEventListener('abort', () => {
reject(
new DOMException(
this.controller.signal.reason || 'Cancelled',
'AbortError'
)
);
});
executor(resolve, reject, this.controller.signal);
});
}
cancel(reason = 'Cancelled') {
this.controller.abort(reason);
}
then(onFulfilled, onRejected) {
return this.promise.then(onFulfilled, onRejected);
}
catch(onRejected) {
return this.promise.catch(onRejected);
}
finally(onFinally) {
return this.promise.finally(onFinally);
}
}
// Usage
const operation = new CancellablePromise(async (resolve, reject, signal) => {
try {
const result = await someAsyncWork(signal);
resolve(result);
} catch (error) {
reject(error);
}
});
operation.then((result) => console.log(result));
operation.cancel('No longer needed');
6. Event Listener Removal
Automatic Cleanup with Signal
const controller = new AbortController();
// Add event listeners with abort signal
window.addEventListener('resize', handleResize, {
signal: controller.signal,
});
document.addEventListener('click', handleClick, {
signal: controller.signal,
});
document.addEventListener('keydown', handleKeydown, {
signal: controller.signal,
});
// Remove all listeners at once
function cleanup() {
controller.abort();
}
// Useful in component cleanup
class MyComponent {
constructor() {
this.abortController = new AbortController();
}
mount() {
const { signal } = this.abortController;
this.element.addEventListener('click', this.onClick, { signal });
window.addEventListener('scroll', this.onScroll, { signal });
document.addEventListener('keyup', this.onKeyUp, { signal });
}
unmount() {
// Single call removes all listeners
this.abortController.abort();
}
}
7. Real-World Patterns
Search with Debounce and Cancellation
class SearchController {
constructor(searchFn, debounceMs = 300) {
this.searchFn = searchFn;
this.debounceMs = debounceMs;
this.currentController = null;
this.debounceTimer = null;
}
search(query) {
// Clear previous debounce timer
if (this.debounceTimer) {
clearTimeout(this.debounceTimer);
}
// Cancel previous request
if (this.currentController) {
this.currentController.abort('New search started');
}
return new Promise((resolve, reject) => {
this.debounceTimer = setTimeout(async () => {
this.currentController = new AbortController();
try {
const result = await this.searchFn(
query,
this.currentController.signal
);
resolve(result);
} catch (error) {
if (error.name !== 'AbortError') {
reject(error);
}
}
}, this.debounceMs);
});
}
cancel() {
if (this.debounceTimer) {
clearTimeout(this.debounceTimer);
}
if (this.currentController) {
this.currentController.abort('Search cancelled');
}
}
}
// Usage
const search = new SearchController(async (query, signal) => {
const response = await fetch(`/api/search?q=${query}`, { signal });
return response.json();
});
// Each keystroke starts a new search, cancelling previous
input.addEventListener('input', async (e) => {
try {
const results = await search.search(e.target.value);
displayResults(results);
} catch (error) {
console.error(error);
}
});
Parallel Requests with Cancellation
async function fetchAllWithCancel(urls) {
const controller = new AbortController();
const promises = urls.map((url) =>
fetch(url, { signal: controller.signal }).then((r) => r.json())
);
return {
promise: Promise.all(promises),
cancel: () => controller.abort(),
};
}
// Usage
const { promise, cancel } = fetchAllWithCancel([
'/api/users',
'/api/posts',
'/api/comments',
]);
// Cancel all if any single request is no longer needed
cancelButton.onclick = cancel;
try {
const [users, posts, comments] = await promise;
} catch (error) {
if (error.name === 'AbortError') {
console.log('All requests cancelled');
}
}
Race Pattern with Cancellation
async function raceWithCancel(promises) {
const controller = new AbortController();
const wrappedPromises = promises.map(({ url, ...options }) =>
fetch(url, { ...options, signal: controller.signal }).then((r) => r.json())
);
try {
const winner = await Promise.race(wrappedPromises);
// Cancel losing requests
controller.abort('Race completed');
return winner;
} catch (error) {
controller.abort('Race failed');
throw error;
}
}
8. Error Handling Best Practices
Distinguishing Abort Errors
async function safeFetch(url, options = {}) {
try {
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
if (error.name === 'AbortError') {
// Request was cancelled - usually not an error
return null;
}
if (error.name === 'TimeoutError') {
// Request timed out
throw new Error('Request timed out. Please try again.');
}
// Network or other error
throw error;
}
}
Creating Typed Abort Errors
class TimeoutAbortError extends DOMException {
constructor(timeout) {
super(`Operation timed out after ${timeout}ms`, 'TimeoutError');
this.timeout = timeout;
}
}
class UserCancelError extends DOMException {
constructor(reason = 'User cancelled') {
super(reason, 'AbortError');
this.isUserCancel = true;
}
}
// Usage
controller.abort(new UserCancelError());
// Handling
catch (error) {
if (error instanceof UserCancelError) {
// User explicitly cancelled
} else if (error.name === 'TimeoutError') {
// Timeout occurred
}
}
Summary
Key Concepts
| Concept | Description |
|---|---|
AbortController | Creates a controller with signal and abort() method |
AbortSignal | Passed to async operations, triggers on abort |
AbortSignal.timeout() | Creates auto-aborting signal after timeout |
AbortSignal.any() | Combines multiple signals |
signal.aborted | Boolean indicating if aborted |
signal.reason | The reason passed to abort() |
Best Practices
- •Always handle AbortError - Don't treat cancellation as an error
- •Clean up resources - Cancel pending operations on component unmount
- •Use timeout - Prevent requests from hanging indefinitely
- •Provide cancel functions - Let users cancel long operations
- •Chain signals - Use combined signals for complex scenarios
Next Steps
- •Practice with the exercises in
exercises.js - •Review real-world examples in
examples.js - •Explore combining with other async patterns