The AbortController API provides a way to cancel fetch requests and other async operations. Here's how to use it.
Basic Usage#
1// Create controller and signal
2const controller = new AbortController();
3const signal = controller.signal;
4
5// Pass signal to fetch
6fetch('/api/data', { signal })
7 .then((response) => response.json())
8 .then((data) => console.log(data))
9 .catch((err) => {
10 if (err.name === 'AbortError') {
11 console.log('Request was cancelled');
12 } else {
13 console.error('Fetch error:', err);
14 }
15 });
16
17// Cancel the request
18controller.abort();With async/await#
1async function fetchWithTimeout(url, timeout = 5000) {
2 const controller = new AbortController();
3
4 // Set timeout to abort
5 const timeoutId = setTimeout(() => controller.abort(), timeout);
6
7 try {
8 const response = await fetch(url, { signal: controller.signal });
9 clearTimeout(timeoutId);
10 return await response.json();
11 } catch (err) {
12 clearTimeout(timeoutId);
13 if (err.name === 'AbortError') {
14 throw new Error('Request timed out');
15 }
16 throw err;
17 }
18}
19
20// Usage
21try {
22 const data = await fetchWithTimeout('/api/slow-endpoint', 3000);
23 console.log(data);
24} catch (err) {
25 console.error(err.message);
26}AbortSignal.timeout()#
1// Built-in timeout signal (newer browsers)
2async function fetchWithTimeout(url, timeout = 5000) {
3 try {
4 const response = await fetch(url, {
5 signal: AbortSignal.timeout(timeout),
6 });
7 return await response.json();
8 } catch (err) {
9 if (err.name === 'TimeoutError') {
10 throw new Error('Request timed out');
11 }
12 if (err.name === 'AbortError') {
13 throw new Error('Request was cancelled');
14 }
15 throw err;
16 }
17}Multiple Requests#
1// Cancel multiple requests with one controller
2const controller = new AbortController();
3
4Promise.all([
5 fetch('/api/users', { signal: controller.signal }),
6 fetch('/api/posts', { signal: controller.signal }),
7 fetch('/api/comments', { signal: controller.signal }),
8])
9 .then(([users, posts, comments]) => {
10 // Process responses
11 })
12 .catch((err) => {
13 if (err.name === 'AbortError') {
14 console.log('All requests cancelled');
15 }
16 });
17
18// Cancel all three requests
19controller.abort();React Integration#
1import { useEffect, useState } from 'react';
2
3function useData(url) {
4 const [data, setData] = useState(null);
5 const [loading, setLoading] = useState(true);
6 const [error, setError] = useState(null);
7
8 useEffect(() => {
9 const controller = new AbortController();
10
11 async function fetchData() {
12 try {
13 setLoading(true);
14 const response = await fetch(url, { signal: controller.signal });
15 const json = await response.json();
16 setData(json);
17 setError(null);
18 } catch (err) {
19 if (err.name !== 'AbortError') {
20 setError(err);
21 }
22 } finally {
23 setLoading(false);
24 }
25 }
26
27 fetchData();
28
29 // Cleanup: abort on unmount or url change
30 return () => controller.abort();
31 }, [url]);
32
33 return { data, loading, error };
34}
35
36// Usage
37function UserProfile({ userId }) {
38 const { data, loading, error } = useData(`/api/users/${userId}`);
39
40 if (loading) return <p>Loading...</p>;
41 if (error) return <p>Error: {error.message}</p>;
42 return <div>{data.name}</div>;
43}Event Listener Removal#
1// Use signal to auto-remove event listeners
2const controller = new AbortController();
3
4window.addEventListener(
5 'resize',
6 () => {
7 console.log('Window resized');
8 },
9 { signal: controller.signal }
10);
11
12window.addEventListener(
13 'scroll',
14 () => {
15 console.log('Window scrolled');
16 },
17 { signal: controller.signal }
18);
19
20// Remove all listeners at once
21controller.abort();
22
23// Useful for component cleanup
24class Component {
25 constructor() {
26 this.controller = new AbortController();
27 }
28
29 mount() {
30 document.addEventListener(
31 'click',
32 this.handleClick,
33 { signal: this.controller.signal }
34 );
35 document.addEventListener(
36 'keydown',
37 this.handleKeydown,
38 { signal: this.controller.signal }
39 );
40 }
41
42 unmount() {
43 // Clean up all listeners
44 this.controller.abort();
45 }
46}AbortSignal.any()#
1// Combine multiple signals (newer browsers)
2const userController = new AbortController();
3const timeoutSignal = AbortSignal.timeout(5000);
4
5// Abort if either user cancels or timeout
6const combinedSignal = AbortSignal.any([
7 userController.signal,
8 timeoutSignal,
9]);
10
11fetch('/api/data', { signal: combinedSignal })
12 .then((response) => response.json())
13 .catch((err) => {
14 if (err.name === 'AbortError') {
15 console.log('Aborted by user');
16 } else if (err.name === 'TimeoutError') {
17 console.log('Request timed out');
18 }
19 });
20
21// User can still cancel manually
22cancelButton.onclick = () => userController.abort();Custom Async Operations#
1// Make any async operation abortable
2function abortableDelay(ms, signal) {
3 return new Promise((resolve, reject) => {
4 // Check if already aborted
5 if (signal?.aborted) {
6 reject(new DOMException('Aborted', 'AbortError'));
7 return;
8 }
9
10 const timeoutId = setTimeout(resolve, ms);
11
12 // Listen for abort
13 signal?.addEventListener('abort', () => {
14 clearTimeout(timeoutId);
15 reject(new DOMException('Aborted', 'AbortError'));
16 });
17 });
18}
19
20// Usage
21const controller = new AbortController();
22
23abortableDelay(5000, controller.signal)
24 .then(() => console.log('Delay complete'))
25 .catch((err) => {
26 if (err.name === 'AbortError') {
27 console.log('Delay cancelled');
28 }
29 });
30
31// Cancel after 2 seconds
32setTimeout(() => controller.abort(), 2000);Abort Reason#
1// Provide reason for abortion
2const controller = new AbortController();
3
4controller.signal.addEventListener('abort', () => {
5 console.log('Abort reason:', controller.signal.reason);
6});
7
8// Abort with reason
9controller.abort('User cancelled');
10// or
11controller.abort(new Error('Custom error'));
12
13// Check reason in catch
14fetch('/api/data', { signal: controller.signal }).catch((err) => {
15 if (err.name === 'AbortError') {
16 console.log('Reason:', controller.signal.reason);
17 }
18});Sequential Request Cancellation#
1// Cancel previous request when new one starts
2let currentController = null;
3
4async function search(query) {
5 // Cancel previous request
6 if (currentController) {
7 currentController.abort();
8 }
9
10 currentController = new AbortController();
11
12 try {
13 const response = await fetch(`/api/search?q=${query}`, {
14 signal: currentController.signal,
15 });
16 return await response.json();
17 } catch (err) {
18 if (err.name === 'AbortError') {
19 return null; // Silently handle cancellation
20 }
21 throw err;
22 }
23}
24
25// Debounced search input
26let debounceTimer;
27searchInput.addEventListener('input', (e) => {
28 clearTimeout(debounceTimer);
29 debounceTimer = setTimeout(() => {
30 search(e.target.value).then((results) => {
31 if (results) {
32 displayResults(results);
33 }
34 });
35 }, 300);
36});Polling with Abort#
1// Abortable polling
2function poll(url, interval, signal) {
3 return new Promise((resolve, reject) => {
4 if (signal?.aborted) {
5 reject(new DOMException('Aborted', 'AbortError'));
6 return;
7 }
8
9 let intervalId;
10
11 const cleanup = () => {
12 clearInterval(intervalId);
13 };
14
15 signal?.addEventListener('abort', () => {
16 cleanup();
17 reject(new DOMException('Aborted', 'AbortError'));
18 });
19
20 intervalId = setInterval(async () => {
21 try {
22 const response = await fetch(url, { signal });
23 const data = await response.json();
24
25 if (data.completed) {
26 cleanup();
27 resolve(data);
28 }
29 } catch (err) {
30 if (err.name !== 'AbortError') {
31 cleanup();
32 reject(err);
33 }
34 }
35 }, interval);
36 });
37}
38
39// Usage
40const controller = new AbortController();
41
42poll('/api/job/123/status', 1000, controller.signal)
43 .then((result) => console.log('Job completed:', result))
44 .catch((err) => console.log('Polling stopped:', err.message));
45
46// Stop polling after 30 seconds
47setTimeout(() => controller.abort(), 30000);File Upload Cancellation#
1// Cancelable file upload
2async function uploadFile(file, onProgress, signal) {
3 const formData = new FormData();
4 formData.append('file', file);
5
6 // XMLHttpRequest for progress
7 return new Promise((resolve, reject) => {
8 const xhr = new XMLHttpRequest();
9
10 xhr.upload.addEventListener('progress', (e) => {
11 if (e.lengthComputable) {
12 onProgress((e.loaded / e.total) * 100);
13 }
14 });
15
16 xhr.addEventListener('load', () => {
17 if (xhr.status >= 200 && xhr.status < 300) {
18 resolve(JSON.parse(xhr.responseText));
19 } else {
20 reject(new Error(`Upload failed: ${xhr.status}`));
21 }
22 });
23
24 xhr.addEventListener('error', () => {
25 reject(new Error('Upload failed'));
26 });
27
28 // Handle abort
29 signal?.addEventListener('abort', () => {
30 xhr.abort();
31 reject(new DOMException('Aborted', 'AbortError'));
32 });
33
34 xhr.open('POST', '/api/upload');
35 xhr.send(formData);
36 });
37}
38
39// Usage
40const controller = new AbortController();
41
42uploadFile(
43 selectedFile,
44 (progress) => console.log(`${progress}%`),
45 controller.signal
46)
47 .then((result) => console.log('Upload complete:', result))
48 .catch((err) => {
49 if (err.name === 'AbortError') {
50 console.log('Upload cancelled');
51 }
52 });
53
54cancelButton.onclick = () => controller.abort();Best Practices#
Usage:
✓ Always handle AbortError
✓ Clean up on component unmount
✓ Use timeout for slow requests
✓ Cancel stale requests
Patterns:
✓ One controller per request group
✓ Check aborted before starting
✓ Provide abort reasons
✓ Combine with debouncing
Event Listeners:
✓ Use signal option
✓ Batch cleanup with abort()
✓ Auto-remove on abort
✓ Cleaner than removeEventListener
Avoid:
✗ Ignoring AbortError
✗ Reusing aborted controllers
✗ Memory leaks from listeners
✗ Missing cleanup in React
Conclusion#
The AbortController API provides a standard way to cancel fetch requests and other async operations. Create a controller, pass its signal to fetch or event listeners, and call abort() when needed. Use it in React effects for cleanup, combine with timeouts for reliability, and handle the AbortError appropriately. The API integrates well with modern JavaScript patterns and helps prevent memory leaks and race conditions.