Why the AbortController API Is a Game‑Changer for JavaScript
The article shows that the standard AbortController API can do far more than just cancel fetch requests—it can cleanly remove event listeners, combine multiple abort signals, control streams, and even make database transactions cancelable, with concrete code examples and compatibility notes.
Usage
AbortController is a global class that creates an AbortController instance exposing two properties: signal – an AbortSignal that can be passed to APIs such as fetch() or event listeners. .abort() – triggers the abort event on the associated signal and marks it as aborted.
Example of creating a controller:
const controller = new AbortController();
controller.signal;
controller.abort();Event listeners
When a signal is supplied to addEventListener, the listener is automatically removed when the signal aborts, eliminating the need for manual removeEventListener() calls.
const controller = new AbortController();
window.addEventListener('resize', listener, { signal: controller.signal });
// Later
controller.abort(); // removes the resize listenerIn a React useEffect hook you can create one controller and abort all attached listeners with a single call:
useEffect(() => {
const controller = new AbortController();
window.addEventListener('resize', handleResize, { signal: controller.signal });
window.addEventListener('hashchange', handleHashChange, { signal: controller.signal });
window.addEventListener('storage', handleStorageChange, { signal: controller.signal });
return () => {
// abort removes all three listeners
controller.abort();
};
}, []);Fetch requests
Passing controller.signal to fetch() aborts the request; the returned promise is rejected with an AbortError.
const controller = new AbortController();
fetch('/upload', { method: 'POST', body: file, signal: controller.signal })
.then(res => res.json())
.catch(err => {
if (err.name === 'AbortError') {
console.log('Upload cancelled');
} else {
console.error('Upload failed', err);
}
});
// Cancel on button click
document.getElementById('cancelButton').addEventListener('click', () => {
controller.abort();
});Node.js http requests also accept a signal property.
const http = require('http');
const { AbortController } = require('abort-controller');
function makeRequest() {
const controller = new AbortController();
const options = { hostname: 'example.com', port: 80, path: '/', method: 'GET', signal: controller.signal };
const req = http.request(options, res => { /* handle response */ });
req.on('error', e => {
if (e.name === 'AbortError') console.log('Request cancelled');
else console.error(`Request error: ${e.message}`);
});
req.end();
setTimeout(() => controller.abort(), 2000); // cancel after 2 s
}
makeRequest();AbortSignal static methods
AbortSignal.timeout(ms)creates a signal that aborts automatically after the given timeout.
fetch(url, { signal: AbortSignal.timeout(1700) })
.then(r => r.json())
.catch(err => {
if (err.name === 'AbortError') console.error('Request timed out');
else console.error('Request error', err);
}); AbortSignal.any([sig1, sig2])combines multiple signals; aborting any of them aborts the combined signal.
const publicController = new AbortController();
const internalController = new AbortController();
const socket = new WebSocket('wss://example.org');
socket.addEventListener('message', handleMessage, {
signal: AbortSignal.any([publicController.signal, internalController.signal])
});
// Cancel via either controller
publicController.abort(); // stops listeningCanceling streams
WritableStream controllers expose a signal that can be listened to for abort events, allowing graceful cancellation of write operations.
const abortController = new AbortController();
const stream = new WritableStream({
write(chunk, controller) {
console.log('Writing:', chunk);
controller.signal.addEventListener('abort', () => {
console.log('Write cancelled');
});
},
close() { console.log('Write complete'); },
abort(reason) { console.warn('Write aborted:', reason); }
});
const writer = stream.getWriter();
writer.write('chunk 1');
writer.write('chunk 2');
window.currentAbortController = abortController;
document.getElementById('cancelButton').addEventListener('click', async () => {
await writer.abort();
abortController.abort();
});Making any logic abortable
By passing an AbortSignal to libraries such as Drizzle ORM, entire transactions become cancelable.
import { TransactionRollbackError } from 'drizzle-orm';
function makeCancelableTransaction(db) {
return (callback, options = {}) => {
return db.transaction(tx => new Promise((resolve, reject) => {
options.signal?.addEventListener('abort', async () => {
reject(new TransactionRollbackError());
});
Promise.resolve(callback.call(this, tx)).then(resolve, reject);
}));
};
};
const controller = new AbortController();
await makeCancelableTransaction(db)(async tx => {
await tx.update(accounts).set({ balance: sql`${accounts.balance} - 100.00` }).where(eq(users.name, 'Dan'));
await tx.update(accounts).set({ balance: sql`${accounts.balance} + 100.00` }).where(eq(users.name, 'Andrew'));
}, { signal: controller.signal });
// controller.abort() rolls back the whole transactionAbort error handling
The reason argument of controller.abort(reason) is exposed via signal.reason, enabling custom error handling.
async function fetchData() {
const controller = new AbortController();
const signal = controller.signal;
signal.addEventListener('abort', () => {
console.log('Abort reason:', signal.reason);
});
try {
const response = await fetch('https://jsonplaceholder.typicode.com/posts/1', { signal });
const data = await response.json();
console.log('Success:', data);
} catch (error) {
if (error.name === 'AbortError') console.error('Request cancelled:', error.message);
else console.error('Request error:', error.message);
}
window.currentAbortController = controller;
}
fetchData();
// Cancel with custom reason
document.getElementById('cancelButton').addEventListener('click', () => {
if (window.currentAbortController) window.currentAbortController.abort('User cancelled');
});Compatibility
AbortController has been part of the web compatibility baseline since March 2019 and works in all major browsers.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Full-Stack Cultivation Path
Focused on sharing practical tech content about TypeScript, Vue 3, front-end architecture, and source code analysis.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
