javascript
examples
examples.js⚡javascript
/**
* AbortController & Cancellation - Examples
*
* Comprehensive examples of cancelling async operations in JavaScript
*/
// ============================================
// EXAMPLE 1: Basic AbortController Usage
// ============================================
/**
* Basic fetch cancellation
*/
function basicAbortExample() {
const controller = new AbortController();
const signal = controller.signal;
// Start the fetch
fetch('https://jsonplaceholder.typicode.com/posts', { signal })
.then((response) => response.json())
.then((data) => console.log('Data received:', data.length, 'posts'))
.catch((error) => {
if (error.name === 'AbortError') {
console.log('Fetch was cancelled');
} else {
console.error('Fetch error:', error);
}
});
// Cancel after 100ms
setTimeout(() => {
controller.abort();
console.log('Abort signal sent');
}, 100);
}
// ============================================
// EXAMPLE 2: Fetch with Timeout
// ============================================
/**
* Fetch wrapper with automatic timeout
*/
async function fetchWithTimeout(url, options = {}, timeout = 5000) {
const controller = new AbortController();
const { signal } = controller;
// Merge any existing signal with our timeout signal
if (options.signal) {
options.signal.addEventListener('abort', () => {
controller.abort(options.signal.reason);
});
}
const timeoutId = setTimeout(() => {
controller.abort(`Request timeout after ${timeout}ms`);
}, timeout);
try {
const response = await fetch(url, { ...options, signal });
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
return response;
} catch (error) {
clearTimeout(timeoutId);
throw error;
}
}
// Usage example
async function fetchWithTimeoutDemo() {
try {
const response = await fetchWithTimeout(
'https://jsonplaceholder.typicode.com/posts',
{},
3000
);
const data = await response.json();
console.log('Fetched with timeout:', data.length, 'posts');
} catch (error) {
if (error.name === 'AbortError') {
console.log('Request timed out:', error.message);
} else {
console.error('Error:', error);
}
}
}
// ============================================
// EXAMPLE 3: Modern Timeout with AbortSignal.timeout()
// ============================================
/**
* Using the built-in AbortSignal.timeout() (ES2022+)
*/
async function modernTimeoutExample() {
try {
// Automatically aborts after 5 seconds
const response = await fetch('https://jsonplaceholder.typicode.com/posts', {
signal: AbortSignal.timeout(5000),
});
const data = await response.json();
console.log('Modern timeout fetch:', data.length, 'posts');
} catch (error) {
if (error.name === 'TimeoutError') {
console.log('Request timed out');
} else if (error.name === 'AbortError') {
console.log('Request aborted');
} else {
console.error('Error:', error);
}
}
}
// ============================================
// EXAMPLE 4: Cancellable Fetch Class
// ============================================
class CancellableFetch {
constructor() {
this.controller = null;
}
async fetch(url, options = {}) {
// Cancel any existing request
this.cancel('New request started');
// Create new controller
this.controller = new AbortController();
try {
const response = await fetch(url, {
...options,
signal: this.controller.signal,
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
if (error.name === 'AbortError') {
return null; // Cancelled, return null
}
throw error;
}
}
cancel(reason = 'Cancelled') {
if (this.controller) {
this.controller.abort(reason);
this.controller = null;
}
}
}
// Usage
const fetcher = new CancellableFetch();
async function cancellableFetchDemo() {
// Start first request
const promise1 = fetcher.fetch(
'https://jsonplaceholder.typicode.com/posts/1'
);
// Start second request (automatically cancels first)
const promise2 = fetcher.fetch(
'https://jsonplaceholder.typicode.com/posts/2'
);
const result = await promise2;
console.log('Got result:', result?.title);
}
// ============================================
// EXAMPLE 5: Search with Auto-Cancel
// ============================================
class SearchManager {
constructor(searchEndpoint) {
this.endpoint = searchEndpoint;
this.controller = null;
this.debounceTimer = null;
this.debounceMs = 300;
}
async search(query) {
// Clear debounce timer
if (this.debounceTimer) {
clearTimeout(this.debounceTimer);
}
// Cancel previous request
if (this.controller) {
this.controller.abort('New search');
}
return new Promise((resolve, reject) => {
this.debounceTimer = setTimeout(async () => {
this.controller = new AbortController();
try {
const url = `${this.endpoint}?q=${encodeURIComponent(query)}`;
const response = await fetch(url, {
signal: this.controller.signal,
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const results = await response.json();
resolve(results);
} catch (error) {
if (error.name !== 'AbortError') {
reject(error);
}
// Silently ignore abort errors
resolve(null);
}
}, this.debounceMs);
});
}
cancel() {
if (this.debounceTimer) {
clearTimeout(this.debounceTimer);
}
if (this.controller) {
this.controller.abort('Search cancelled');
}
}
}
// Simulated usage
const searchManager = new SearchManager('/api/search');
// ============================================
// EXAMPLE 6: Combining Multiple Signals
// ============================================
/**
* Combine multiple abort signals into one
* Polyfill for AbortSignal.any()
*/
function combineAbortSignals(...signals) {
const controller = new AbortController();
const onAbort = function () {
controller.abort(this.reason);
cleanup();
};
const cleanup = () => {
signals.forEach((signal) => {
signal.removeEventListener('abort', onAbort);
});
};
for (const signal of signals) {
if (signal.aborted) {
controller.abort(signal.reason);
return controller.signal;
}
signal.addEventListener('abort', onAbort);
}
return controller.signal;
}
// Usage: Cancel on either user action OR timeout
async function fetchWithCombinedSignals(url) {
const userController = new AbortController();
// Create timeout signal
const timeoutSignal = AbortSignal.timeout(10000);
// Combine signals
const combinedSignal = combineAbortSignals(
userController.signal,
timeoutSignal
);
// Expose cancel function
const cancel = () => userController.abort('User cancelled');
try {
const response = await fetch(url, { signal: combinedSignal });
return await response.json();
} catch (error) {
if (error.name === 'AbortError') {
console.log('Request aborted:', error.message);
}
throw error;
}
}
// ============================================
// EXAMPLE 7: Cancellable Promise Wrapper
// ============================================
class CancellablePromise {
constructor(executor) {
this.controller = new AbortController();
this.settled = false;
this.promise = new Promise((resolve, reject) => {
// Handle abort
this.controller.signal.addEventListener('abort', () => {
if (!this.settled) {
reject(
new DOMException(
this.controller.signal.reason || 'Cancelled',
'AbortError'
)
);
}
});
// Execute with signal
const wrappedResolve = (value) => {
this.settled = true;
resolve(value);
};
const wrappedReject = (error) => {
this.settled = true;
reject(error);
};
try {
executor(wrappedResolve, wrappedReject, this.controller.signal);
} catch (error) {
wrappedReject(error);
}
});
}
cancel(reason = 'Cancelled') {
if (!this.settled) {
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
function cancellablePromiseDemo() {
const operation = new CancellablePromise(async (resolve, reject, signal) => {
// Simulate long operation
for (let i = 0; i < 10; i++) {
if (signal.aborted) {
reject(new DOMException('Cancelled', 'AbortError'));
return;
}
await new Promise((r) => setTimeout(r, 500));
}
resolve('Operation complete');
});
operation
.then((result) => console.log(result))
.catch((error) => {
if (error.name === 'AbortError') {
console.log('Operation was cancelled');
}
});
// Cancel after 2 seconds
setTimeout(() => operation.cancel('Took too long'), 2000);
}
// ============================================
// EXAMPLE 8: Event Listener Cleanup with Signal
// ============================================
class ComponentWithEvents {
constructor(element) {
this.element = element;
this.abortController = new AbortController();
}
mount() {
const { signal } = this.abortController;
// All these listeners will be removed together
this.element.addEventListener('click', this.handleClick.bind(this), {
signal,
});
this.element.addEventListener(
'mouseenter',
this.handleMouseEnter.bind(this),
{ signal }
);
this.element.addEventListener(
'mouseleave',
this.handleMouseLeave.bind(this),
{ signal }
);
window.addEventListener('resize', this.handleResize.bind(this), { signal });
document.addEventListener('keydown', this.handleKeyDown.bind(this), {
signal,
});
console.log('Component mounted with event listeners');
}
unmount() {
// Single call removes ALL listeners
this.abortController.abort();
console.log('Component unmounted, all listeners removed');
}
handleClick(e) {
console.log('Click:', e.target);
}
handleMouseEnter() {
console.log('Mouse enter');
}
handleMouseLeave() {
console.log('Mouse leave');
}
handleResize() {
console.log('Resize');
}
handleKeyDown(e) {
console.log('Key:', e.key);
}
}
// ============================================
// EXAMPLE 9: Parallel Fetch with Single Cancel
// ============================================
class ParallelFetcher {
constructor() {
this.controller = null;
}
async fetchAll(urls) {
// Cancel any previous batch
this.cancel();
this.controller = new AbortController();
const { signal } = this.controller;
const promises = urls.map(async (url) => {
const response = await fetch(url, { signal });
if (!response.ok) {
throw new Error(`HTTP ${response.status} for ${url}`);
}
return response.json();
});
return Promise.all(promises);
}
async fetchRace(urls) {
this.cancel();
this.controller = new AbortController();
const { signal } = this.controller;
const promises = urls.map(async (url) => {
const response = await fetch(url, { signal });
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
// Cancel other requests once we have a winner
this.controller.abort('Race won');
return data;
});
return Promise.race(promises);
}
cancel(reason = 'Cancelled') {
if (this.controller) {
this.controller.abort(reason);
this.controller = null;
}
}
}
// ============================================
// EXAMPLE 10: Retry with Cancellation
// ============================================
async function fetchWithRetry(url, options = {}) {
const {
maxRetries = 3,
retryDelay = 1000,
timeout = 5000,
signal,
...fetchOptions
} = options;
const controller = new AbortController();
// Link external signal if provided
if (signal) {
signal.addEventListener('abort', () => {
controller.abort(signal.reason);
});
if (signal.aborted) {
throw new DOMException(signal.reason || 'Aborted', 'AbortError');
}
}
let lastError;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
// Create timeout for this attempt
const timeoutId = setTimeout(() => {
controller.abort('Request timeout');
}, timeout);
const response = await fetch(url, {
...fetchOptions,
signal: controller.signal,
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
lastError = error;
// Don't retry on abort
if (error.name === 'AbortError') {
throw error;
}
// Don't retry on last attempt
if (attempt === maxRetries) {
break;
}
console.log(
`Attempt ${attempt + 1} failed, retrying in ${retryDelay}ms...`
);
// Wait before retry
await new Promise((resolve) => setTimeout(resolve, retryDelay));
}
}
throw lastError;
}
// ============================================
// EXAMPLE 11: Cancellable Async Iterator
// ============================================
async function* cancellablePagination(baseUrl, signal) {
let page = 1;
let hasMore = true;
while (hasMore) {
// Check if cancelled before each request
if (signal?.aborted) {
throw new DOMException(
signal.reason || 'Iteration cancelled',
'AbortError'
);
}
const response = await fetch(`${baseUrl}?page=${page}`, { signal });
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
if (data.items.length === 0) {
hasMore = false;
} else {
yield data.items;
page++;
}
}
}
// Usage
async function paginationDemo() {
const controller = new AbortController();
try {
const paginator = cancellablePagination(
'https://api.example.com/items',
controller.signal
);
for await (const items of paginator) {
console.log('Got page with', items.length, 'items');
// Stop after 3 pages
if (items.pageNumber >= 3) {
controller.abort('Enough pages');
}
}
} catch (error) {
if (error.name === 'AbortError') {
console.log('Pagination stopped:', error.message);
}
}
}
// ============================================
// EXAMPLE 12: Upload with Progress and Cancel
// ============================================
class CancellableUpload {
constructor() {
this.controller = null;
this.onProgress = null;
}
async upload(url, file, onProgress) {
this.cancel();
this.controller = new AbortController();
this.onProgress = onProgress;
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
// Handle abort
this.controller.signal.addEventListener('abort', () => {
xhr.abort();
reject(
new DOMException(
this.controller.signal.reason || 'Upload cancelled',
'AbortError'
)
);
});
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable && this.onProgress) {
const percent = (e.loaded / e.total) * 100;
this.onProgress(percent, e.loaded, e.total);
}
});
xhr.addEventListener('load', () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(JSON.parse(xhr.responseText));
} else {
reject(new Error(`Upload failed: ${xhr.status}`));
}
});
xhr.addEventListener('error', () => {
reject(new Error('Upload failed'));
});
xhr.open('POST', url);
const formData = new FormData();
formData.append('file', file);
xhr.send(formData);
});
}
cancel(reason = 'Upload cancelled') {
if (this.controller) {
this.controller.abort(reason);
this.controller = null;
}
}
}
// ============================================
// EXAMPLE 13: WebSocket with Abort
// ============================================
class CancellableWebSocket {
constructor(url) {
this.url = url;
this.ws = null;
this.controller = new AbortController();
}
connect() {
return new Promise((resolve, reject) => {
this.controller.signal.addEventListener('abort', () => {
if (this.ws) {
this.ws.close();
}
reject(new DOMException('Connection cancelled', 'AbortError'));
});
if (this.controller.signal.aborted) {
reject(new DOMException('Already aborted', 'AbortError'));
return;
}
this.ws = new WebSocket(this.url);
this.ws.onopen = () => resolve(this.ws);
this.ws.onerror = (error) => reject(error);
});
}
send(data) {
if (this.controller.signal.aborted) {
throw new DOMException('Connection closed', 'AbortError');
}
this.ws?.send(JSON.stringify(data));
}
close(reason = 'Closing connection') {
this.controller.abort(reason);
this.ws?.close();
}
}
// ============================================
// DEMONSTRATION
// ============================================
function demonstrateAbortController() {
console.log('=== AbortController & Cancellation Examples ===\n');
console.log('Available examples:');
const examples = [
'basicAbortExample() - Basic fetch cancellation',
'fetchWithTimeoutDemo() - Timeout wrapper',
'modernTimeoutExample() - AbortSignal.timeout()',
'cancellableFetchDemo() - Auto-cancelling fetcher',
'cancellablePromiseDemo() - Cancellable Promise class',
'CancellableFetch - Class for managing requests',
'SearchManager - Debounced search with cancel',
'ComponentWithEvents - Event listener cleanup',
'ParallelFetcher - Cancel multiple requests',
'fetchWithRetry() - Retry with cancellation',
'CancellableUpload - File upload with progress',
'CancellableWebSocket - WebSocket with abort',
];
examples.forEach((ex, i) => console.log(`${i + 1}. ${ex}`));
console.log('\nKey APIs:');
console.log('- new AbortController()');
console.log('- controller.signal');
console.log('- controller.abort(reason)');
console.log('- signal.aborted');
console.log('- signal.reason');
console.log('- AbortSignal.timeout(ms)');
console.log('- AbortSignal.any([signals])');
}
// Export for module usage
if (typeof module !== 'undefined' && module.exports) {
module.exports = {
fetchWithTimeout,
CancellableFetch,
SearchManager,
combineAbortSignals,
CancellablePromise,
ComponentWithEvents,
ParallelFetcher,
fetchWithRetry,
cancellablePagination,
CancellableUpload,
CancellableWebSocket,
demonstrateAbortController,
};
}