JavaScript Promises Tutorial - Section 5: Advanced Patterns
Cancel async operations when:
// Create controller
const controller = new AbortController();
const signal = controller.signal;
// Use with fetch
fetch('/api/data', { signal })
.then(response => response.json())
.then(data => console.log(data))
.catch(error => {
if (error.name === 'AbortError') {
console.log('Request cancelled');
} else {
console.error('Request failed:', error);
}
});
// Cancel the request
controller.abort();
async function fetchWithCancel(url) {
const controller = new AbortController();
const fetchPromise = fetch(url, { signal: controller.signal })
.then(response => response.json());
// Return both promise and cancel function
return {
promise: fetchPromise,
cancel: () => controller.abort()
};
}
// Usage
const { promise, cancel } = fetchWithCancel('/api/data');
promise
.then(data => console.log(data))
.catch(error => {
if (error.name === 'AbortError') {
console.log('Cancelled');
}
});
// Cancel after 5 seconds
setTimeout(() => cancel(), 5000);
function makeCancellable(asyncFn) {
const controller = new AbortController();
const promise = asyncFn(controller.signal);
return {
promise,
cancel: () => controller.abort()
};
}
// Usage
async function longRunningTask(signal) {
for (let i = 0; i < 100; i++) {
// Check if cancelled
if (signal.aborted) {
throw new Error('Operation cancelled');
}
await processItem(i);
}
return 'Complete';
}
const { promise, cancel } = makeCancellable(longRunningTask);
promise
.then(result => console.log(result))
.catch(error => console.error(error.message));
// Cancel after 2 seconds
setTimeout(() => cancel(), 2000);
async function cancellableOperation(signal) {
// Listen for abort event
signal.addEventListener('abort', () => {
console.log('Operation cancelled, cleaning up...');
cleanup();
});
try {
const data = await fetch('/api/data', { signal });
return await data.json();
} catch (error) {
if (error.name === 'AbortError') {
console.log('Fetch was cancelled');
}
throw error;
}
}
const controller = new AbortController();
cancellableOperation(controller.signal);
// Cancel
controller.abort();
function fetchWithTimeout(url, timeout = 5000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => {
controller.abort();
}, timeout);
return fetch(url, { signal: controller.signal })
.then(response => {
clearTimeout(timeoutId);
return response.json();
})
.catch(error => {
clearTimeout(timeoutId);
if (error.name === 'AbortError') {
throw new Error('Request timed out');
}
throw error;
});
}
// Usage
try {
const data = await fetchWithTimeout('/api/data', 3000);
console.log(data);
} catch (error) {
console.error(error.message);
}
class CancellablePromise {
constructor(executor) {
this.controller = new AbortController();
this.signal = this.controller.signal;
this.promise = new Promise((resolve, reject) => {
this.signal.addEventListener('abort', () => {
reject(new Error('Cancelled'));
});
executor(resolve, reject, this.signal);
});
}
cancel() {
this.controller.abort();
}
then(onFulfilled, onRejected) {
return this.promise.then(onFulfilled, onRejected);
}
catch(onRejected) {
return this.promise.catch(onRejected);
}
}
// Usage
const cancellable = new CancellablePromise((resolve, reject, signal) => {
const timeoutId = setTimeout(() => {
if (!signal.aborted) {
resolve('Done');
}
}, 5000);
signal.addEventListener('abort', () => {
clearTimeout(timeoutId);
});
});
cancellable
.then(result => console.log(result))
.catch(error => console.error(error.message));
// Cancel after 2 seconds
setTimeout(() => cancellable.cancel(), 2000);
class CancellableAPIClient {
constructor(baseURL) {
this.baseURL = baseURL;
this.pendingRequests = new Map();
}
async request(endpoint, options = {}) {
const controller = new AbortController();
const requestId = `${options.method || 'GET'}-${endpoint}`;
// Cancel previous request to same endpoint
if (this.pendingRequests.has(requestId)) {
this.pendingRequests.get(requestId).abort();
}
this.pendingRequests.set(requestId, controller);
try {
const response = await fetch(`${this.baseURL}${endpoint}`, {
...options,
signal: controller.signal
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
this.pendingRequests.delete(requestId);
return data;
} catch (error) {
this.pendingRequests.delete(requestId);
if (error.name === 'AbortError') {
throw new Error('Request cancelled');
}
throw error;
}
}
cancelAll() {
this.pendingRequests.forEach(controller => controller.abort());
this.pendingRequests.clear();
}
cancel(endpoint, method = 'GET') {
const requestId = `${method}-${endpoint}`;
const controller = this.pendingRequests.get(requestId);
if (controller) {
controller.abort();
this.pendingRequests.delete(requestId);
}
}
}
// Usage
const api = new CancellableAPIClient('https://api.example.com');
// Start request
api.request('/users')
.then(users => console.log(users))
.catch(error => console.error(error.message));
// Cancel specific request
api.cancel('/users');
// Or cancel all
api.cancelAll();
function useCancellableFetch(url) {
const [data, setData] = React.useState(null);
const [loading, setLoading] = React.useState(true);
const [error, setError] = React.useState(null);
React.useEffect(() => {
const controller = new AbortController();
fetch(url, { signal: controller.signal })
.then(response => response.json())
.then(data => {
setData(data);
setLoading(false);
})
.catch(error => {
if (error.name !== 'AbortError') {
setError(error);
setLoading(false);
}
});
// Cleanup: cancel on unmount
return () => controller.abort();
}, [url]);
return { data, loading, error };
}
// Usage in component
function UserProfile({ userId }) {
const { data, loading, error } = useCancellableFetch(
`/api/users/${userId}`
);
if (loading) return Loading...;
if (error) return Error: {error.message};
return {data.name};
}
class DebouncedSearch {
constructor(searchFn, delay = 300) {
this.searchFn = searchFn;
this.delay = delay;
this.timeoutId = null;
this.controller = null;
}
search(query) {
// Cancel previous search
if (this.controller) {
this.controller.abort();
}
// Clear previous timeout
clearTimeout(this.timeoutId);
return new Promise((resolve, reject) => {
this.timeoutId = setTimeout(async () => {
this.controller = new AbortController();
try {
const results = await this.searchFn(
query,
this.controller.signal
);
resolve(results);
} catch (error) {
if (error.name !== 'AbortError') {
reject(error);
}
}
}, this.delay);
});
}
cancel() {
clearTimeout(this.timeoutId);
if (this.controller) {
this.controller.abort();
}
}
}
// Usage
const search = new DebouncedSearch(
async (query, signal) => {
const response = await fetch(`/api/search?q=${query}`, { signal });
return await response.json();
},
300
);
// User typing
input.addEventListener('input', async (e) => {
try {
const results = await search.search(e.target.value);
displayResults(results);
} catch (error) {
console.error(error);
}
});
function combineSignals(...signals) {
const controller = new AbortController();
for (const signal of signals) {
if (signal.aborted) {
controller.abort();
break;
}
signal.addEventListener('abort', () => controller.abort());
}
return controller.signal;
}
// Usage
const userController = new AbortController();
const timeoutController = new AbortController();
setTimeout(() => timeoutController.abort(), 5000);
const combinedSignal = combineSignals(
userController.signal,
timeoutController.signal
);
fetch('/api/data', { signal: combinedSignal })
.then(response => response.json())
.catch(error => {
if (error.name === 'AbortError') {
console.log('Cancelled by user or timeout');
}
});
signal.aborted in long operationsNext, we'll learn about Promise memoization and caching strategies!