Back to Blog
JavaScriptAbortControllerFetchAsync

JavaScript AbortController API Guide

Master the JavaScript AbortController API for cancelling fetch requests and other async operations.

B
Bootspring Team
Engineering
September 8, 2019
7 min read

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.

Share this article

Help spread the word about Bootspring