乐闻世界logo
搜索文章和话题

How to implement Promise cancellation?

2月22日 14:07

Promise cancellation is a common but complex problem. Promises themselves don't support cancellation, but we can achieve similar functionality through various techniques.

Why Can't Promises Be Cancelled?

Promise design follows the "irreversible" principle:

  • Once a Promise state changes (pending → fulfilled or pending → rejected), it cannot change again
  • This design ensures Promise reliability and predictability
  • Cancellation operations introduce additional complexity and uncertainty

Several Methods to Implement Cancellation

1. Using AbortController

AbortController is a standard API provided by modern browsers for cancelling asynchronous operations.

Basic Usage

javascript
const controller = new AbortController(); const signal = controller.signal; 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 request controller.abort();

Wrapper for Cancellable fetch

javascript
function fetchWithTimeout(url, options = {}, timeout = 5000) { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeout); return fetch(url, { ...options, signal: controller.signal }) .then(response => { clearTimeout(timeoutId); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response.json(); }) .catch(error => { clearTimeout(timeoutId); if (error.name === 'AbortError') { throw new Error('Request timeout'); } throw error; }); } // Usage example fetchWithTimeout('/api/data', {}, 3000) .then(data => console.log(data)) .catch(error => console.error(error));

2. Using Wrapper Functions

Implement cancellation functionality through wrapper functions.

Basic Implementation

javascript
function makeCancellable(promise) { let hasCancelled = false; const wrappedPromise = new Promise((resolve, reject) => { promise.then( value => { if (!hasCancelled) resolve(value); }, error => { if (!hasCancelled) reject(error); } ); }); return { promise: wrappedPromise, cancel: () => { hasCancelled = true; } }; } // Usage example const { promise, cancel } = makeCancellable( fetch('/api/data').then(r => r.json()) ); promise .then(data => console.log(data)) .catch(error => { if (error.name === 'CancellationError') { console.log('Operation cancelled'); } else { console.error('Operation failed:', error); } }); // Cancel operation cancel();

Complete Implementation

javascript
class CancellablePromise { constructor(executor) { this.isCancelled = false; this.rejectors = []; this.promise = new Promise((resolve, reject) => { this.resolve = resolve; this.reject = reject; executor( value => { if (!this.isCancelled) { resolve(value); } }, error => { if (!this.isCancelled) { reject(error); } } ); }); } cancel() { this.isCancelled = true; this.rejectors.forEach(rejector => { rejector(new Error('Promise cancelled')); }); } then(onFulfilled, onRejected) { const promise = this.promise.then(onFulfilled, onRejected); return new CancellablePromise((resolve, reject) => { this.rejectors.push(reject); promise.then(resolve, reject); }); } catch(onRejected) { return this.then(null, onRejected); } finally(onFinally) { return this.then( value => { onFinally(); return value; }, error => { onFinally(); throw error; } ); } } // Usage example const cancellablePromise = new CancellablePromise((resolve) => { setTimeout(() => { resolve('Completed'); }, 2000); }); cancellablePromise .then(result => console.log(result)) .catch(error => console.error(error)); // Cancel setTimeout(() => { cancellablePromise.cancel(); }, 1000);

3. Using Token Pattern

Check whether to continue execution by passing a token.

javascript
class CancellationToken { constructor() { this.isCancelled = false; } cancel() { this.isCancelled = true; } throwIfCancelled() { if (this.isCancelled) { throw new Error('Operation cancelled'); } } } function fetchWithToken(url, token) { return fetch(url) .then(response => { token.throwIfCancelled(); return response.json(); }) .then(data => { token.throwIfCancelled(); return data; }); } // Usage example const token = new CancellationToken(); fetchWithToken('/api/data', token) .then(data => console.log(data)) .catch(error => { if (error.message === 'Operation cancelled') { console.log('Operation cancelled'); } else { console.error('Operation failed:', error); } }); // Cancel operation setTimeout(() => { token.cancel(); }, 1000);

4. Using Promise.race for Timeout

javascript
function promiseWithTimeout(promise, timeout = 5000) { const timeoutPromise = new Promise((_, reject) => { setTimeout(() => { reject(new Error('Timeout')); }, timeout); }); return Promise.race([promise, timeoutPromise]); } // Usage example promiseWithTimeout( fetch('/api/data').then(r => r.json()), 3000 ) .then(data => console.log(data)) .catch(error => { if (error.message === 'Timeout') { console.log('Request timeout'); } else { console.error('Request failed:', error); } });

Practical Use Cases

javascript
class SearchService { constructor() { this.currentController = null; } async search(query) { // Cancel previous request if (this.currentController) { this.currentController.abort(); } this.currentController = new AbortController(); try { const response = await fetch(`/api/search?q=${query}`, { signal: this.currentController.signal }); return await response.json(); } catch (error) { if (error.name === 'AbortError') { console.log('Search request cancelled'); return null; } throw error; } } } // Usage example const searchService = new SearchService(); // User types quickly, only keep the last search searchService.search('hello'); searchService.search('hello world'); searchService.search('hello world example');

2. Cancel Long-Running Tasks

javascript
class TaskManager { constructor() { this.tasks = new Map(); } async runTask(taskId, task) { const controller = new AbortController(); this.tasks.set(taskId, controller); try { const result = await task(controller.signal); return result; } catch (error) { if (error.name === 'AbortError') { console.log(`Task ${taskId} cancelled`); return null; } throw error; } finally { this.tasks.delete(taskId); } } cancelTask(taskId) { const controller = this.tasks.get(taskId); if (controller) { controller.abort(); } } } // Usage example const taskManager = new TaskManager(); // Run task taskManager.runTask('task1', async (signal) => { for (let i = 0; i < 10; i++) { signal.throwIfAborted(); await new Promise(resolve => setTimeout(resolve, 1000)); console.log(`Step ${i + 1} completed`); } return 'Task completed'; }); // Cancel task setTimeout(() => { taskManager.cancelTask('task1'); }, 3000);

3. Cancel Requests on Component Unmount

javascript
class Component { constructor() { this.controller = new AbortController(); } async fetchData() { try { const response = await fetch('/api/data', { signal: this.controller.signal }); return await response.json(); } catch (error) { if (error.name === 'AbortError') { console.log('Component unmounted, request cancelled'); return null; } throw error; } } destroy() { this.controller.abort(); } } // Usage example const component = new Component(); component.fetchData().then(data => { if (data) { console.log('Data loaded successfully:', data); } }); // When component unmounts setTimeout(() => { component.destroy(); }, 1000);

Best Practices

1. Always Clean Up Resources

javascript
async function fetchData() { const controller = new AbortController(); try { const response = await fetch('/api/data', { signal: controller.signal }); return await response.json(); } finally { controller.abort(); } }

2. Provide Cancellation Callback

javascript
function fetchWithCancel(url, onCancel) { const controller = new AbortController(); const promise = fetch(url, { signal: controller.signal }) .then(response => response.json()); promise.cancel = () => { controller.abort(); if (onCancel) { onCancel(); } }; return promise; } // Usage example const promise = fetchWithCancel('/api/data', () => { console.log('Request cancelled'); }); promise.then(data => console.log(data)); // Cancel request promise.cancel();

3. Handle Cancellation Errors

javascript
async function fetchWithCancellation(url) { const controller = new AbortController(); try { const response = await fetch(url, { signal: controller.signal }); return await response.json(); } catch (error) { if (error.name === 'AbortError') { console.log('Request cancelled'); return null; } throw error; } }

Summary

  1. Promises don't support cancellation natively: Need to implement through other means
  2. AbortController is the standard solution: Recommended for modern browsers
  3. Wrapper functions provide flexibility: Can customize cancellation logic based on needs
  4. Token pattern suits complex scenarios: Can finely control cancellation timing
  5. Always clean up resources: Avoid memory leaks
  6. Handle cancellation errors: Distinguish between cancellation errors and other errors
  7. Provide cancellation callback: Let the caller know the operation was cancelled
  8. Consider user experience: Cancellation operations should respond quickly
标签:Promise