easy-cancelable-promise
v2.0.1
Published
CancelablePromise is a Promise that can be canceled. It is a Promise that has a status property that can be '`pending`', '`resolved`', '`rejected`' or '`canceled`'. It has an onCancel method that allows you to register a callback that will be called when
Maintainers
Readme
easy-cancelable-promise 🎯
![]()
The cancelable promise you didn't know you needed. 🚀
Promises that respect boundaries. Cancel what you don't need. ✨
🎯 The Problem
You're fetching user data, but the user navigated away. Your fetch continues anyway. Memory leak. Wasted bandwidth. Potential race conditions.
Native promises can't be canceled. Their status can't be tracked. Once started, they run to completion. Always.
💡 The Solution
import { CancelablePromise } from 'easy-cancelable-promise';
const fetchUser = new CancelablePromise(
async (resolve, reject, { onCancel }) => {
const controller = new AbortController();
onCancel(() => controller.abort());
const user = await fetch('/api/user', controller).then((res) => res.json());
resolve(user);
},
);
// User navigated? Just cancel it.
fetchUser.cancel('User navigated away');Clean. Simple. The promise handles its own cleanup. Cancel from anywhere, anytime. 🎯
🚀 Why Developers Love This Library
🎓 100% Promise Compatible
// If you know this...
const promise = new Promise((resolve, reject) => {
// ...
});
// You know this!
const promise = new CancelablePromise((resolve, reject, { onCancel }) => {
// ...
});Works with async/await, .then(), .catch() - everything!
⚡ Built-in Progress Tracking
const download = new CancelablePromise(
(resolve, reject, { reportProgress }) => {
// Report progress as you go
reportProgress(25);
reportProgress(50);
reportProgress(100);
resolve('Done!');
},
);
const result = await download.onProgress((percent) => {
console.log(`${percent}% complete`);
});
console.log('Finished:', result);🎯 Lifecycle Control
const task = new CancelablePromise((resolve, reject, { onCancel }) => {
const resource = allocate();
onCancel(() => {
resource.cleanup();
console.log('Cleaned up!');
});
// Do work...
});
task.cancel(); // Cleanup happens automatically🔍 Status Tracking
console.log(promise.status); // 'pending'
await promise;
console.log(promise.status); // 'resolved'
promise.cancel();
console.log(promise.status); // 'canceled'Track state throughout the entire lifecycle!
🎪 Multiple Cleanup Strategies
const download = new CancelablePromise(
async (resolve, reject, { onCancel }) => {
const controller = new AbortController();
let cleanup = onCancel(() => controller.abort());
const file = await fetch(url, { signal: controller.signal });
// there is nothing to abort anymore, so we can remove the old listener
cleanup();
cleanup = onCancel(() => tempFile.delete());
await saveFile(file);
},
);🔗 Utilities Included
import {
defer,
groupAsCancelablePromise,
CancelablePromise,
} from 'easy-cancelable-promise';
// Defer - external promise control
const deferred = defer<User>();
button.onclick = () => deferred.resolve(userData);
// Group - batch with concurrency
const batch = groupAsCancelablePromise(
[() => fetchUser(1), () => fetchUser(2), () => fetchUser(3)],
{ maxConcurrent: 2 },
);
// Static methods - just like Promise
const all = CancelablePromise.all([promise1, promise2]);
const race = CancelablePromise.race([promise1, promise2]);📦 Installation
npm install easy-cancelable-promiseZero dependencies. TypeScript ready. Works everywhere.
🎬 Quick Start
The Basics
import { CancelablePromise } from 'easy-cancelable-promise';
const loadUserData = new CancelablePromise(
async (resolve, reject, { onCancel }) => {
const controller = new AbortController();
onCancel(() => controller.abort());
const data = await fetch('/api/user', { signal: controller.signal });
resolve(await data.json());
},
);
// Cancel anytime
loadUserData.cancel('User navigated away');Works exactly like Promise, but with superpowers. ⚡
Multi-Stage Operations with Cascading Cancellation
Cancel operations at different stages based on progress:
const processData = new CancelablePromise(
async (resolve, reject, { onCancel }) => {
// Stage 1: Fetch data from API
const fetchData = api.fetch(requestId);
onCancel(() => fetchData.cancel('Request canceled during fetch'));
const data = await fetchData;
// Stage 2: Transform and save
const saveData = api.save(data);
onCancel(() => saveData.cancel('Request canceled during save'));
const result = await saveData;
resolve(result);
},
);
// User cancels during any stage? Proper cleanup happens automatically
cancelButton.onclick = () => processData.cancel('User canceled operation');Dynamic cleanup strategies that evolve with your operation. 🎯
Progress Tracking
const uploadFile = new CancelablePromise(
(resolve, reject, { onCancel, reportProgress }) => {
const xhr = new XMLHttpRequest();
xhr.upload.onprogress = (e) => {
reportProgress((e.loaded / e.total) * 100);
};
onCancel(() => xhr.abort());
xhr.onload = () => resolve(xhr.response);
xhr.onerror = () => reject(new Error('Upload failed'));
xhr.open('POST', '/upload');
xhr.send(fileData);
},
);
uploadFile
.onProgress((percent) => {
progressBar.style.width = `${percent}%`;
})
.then(() => showSuccess())
.catch(() => showError());
// Or with async/await
try {
await uploadFile.onProgress((percent) => {
progressBar.style.width = `${percent}%`;
});
showSuccess();
} catch (error) {
showError();
}
// User clicks cancel
uploadFile.cancel();Built-in progress tracking. No extra libraries needed. 📊
🌟 Core Features Deep Dive
1️⃣ CancelablePromise - The Foundation
The core class that extends native Promise with cancellation and lifecycle management.
🎨 The Basics
import { CancelablePromise } from 'easy-cancelable-promise';
// Simple timeout
const timeout = new CancelablePromise((resolve) => {
setTimeout(() => resolve('Done!'), 1000);
});
// With cancellation
const withCancel = new CancelablePromise((resolve, reject, { onCancel }) => {
const id = setTimeout(() => resolve('Done!'), 5000);
onCancel((reason) => {
clearTimeout(id);
console.log('Canceled because:', reason);
});
});
withCancel.cancel('User navigated away');🎯 Executor Utilities
The third parameter gives you superpowers:
new CancelablePromise(
(
resolve,
reject,
{ cancel, onCancel, reportProgress, status, isCanceled, isPending },
) => {
// ✅ cancel: Cancel from inside
cancel('Internal cancellation');
// ✅ onCancel: Subscribe to cancellation
const cleanup = onCancel((reason) => {
console.log('Canceled:', reason);
});
// ✅ reportProgress: Report progress
reportProgress(50); // 50% complete
// ✅ status: Get current status
console.log(status()); // 'pending'
// ✅ isCanceled: Check if canceled
if (isCanceled()) return;
// ✅ isPending: Check if still pending
if (isPending()) {
// Continue work
}
},
);⚡ Status Tracking
Track your promise through its entire lifecycle:
const promise = new CancelablePromise((resolve) => {
setTimeout(() => resolve('Done!'), 1000);
});
console.log(promise.status); // 'pending'
promise.then(() => {
console.log(promise.status); // 'resolved'
});
// Or if canceled
promise.cancel();
console.log(promise.status); // 'canceled'
// On error
promise.catch(() => {
console.log(promise.status); // 'rejected'
});Status types:
'pending'- In progress'resolved'- Successfully completed'rejected'- Failed with error'canceled'- Canceled by user/system
🎬 Lifecycle Hooks
Subscribe to cancellation from inside or outside:
// Inside the executor
const promise = new CancelablePromise((resolve, reject, { onCancel }) => {
const socket = createSocket();
onCancel(() => {
socket.close();
console.log('Socket closed');
});
socket.on('data', (data) => resolve(data));
});
// Outside the executor
promise.onCancel((reason) => {
console.log('Canceled because:', reason);
logToAnalytics('promise_canceled', { reason });
});🎨 Progress Tracking
Report and track progress throughout execution:
const task = new CancelablePromise((resolve, reject, { reportProgress }) => {
const steps = 10;
for (let i = 0; i < steps; i++) {
doWork(i);
reportProgress((i / steps) * 100);
}
resolve('Complete!');
});
// Track progress
task.onProgress((percent, metadata) => {
updateProgressBar(percent);
console.log(`${percent}% complete`, metadata);
});
// Chain progress tracking
task
.onProgress((p) => console.log(`Progress: ${p}%`))
.onProgress((p) => updateUI(p))
.then((result) => console.log('Done!', result));🎪 Cancel from Anywhere
Cancel from inside the executor or outside:
const fetchWithTimeout = new CancelablePromise(
async (resolve, reject, { cancel, onCancel }) => {
const controller = new AbortController();
onCancel(() => controller.abort());
// control it's own timeout
const timeoutId = setTimeout(() => {
cancel('Request timeout');
}, 5000);
try {
const response = await fetch('/api/data', { signal: controller.signal });
clearTimeout(timeoutId);
resolve(await response.json());
} catch (error) {
clearTimeout(timeoutId);
reject(error);
}
},
);
// Cancel from outside (internal timeout also cancels automatically)
cancelButton.onclick = () => {
fetchWithTimeout.cancel('User canceled');
};
// Cancellation won't cause unhandled rejection, but you can catch it:
fetchWithTimeout.catch((error) => {
console.log('Request failed or canceled:', error);
});2️⃣ defer - Deferred Promises
Create promises with externalized resolve/reject control:
import { defer } from 'easy-cancelable-promise';
// Basic usage
const deferred = defer<string>();
deferred.promise.then((result) => {
console.log('Result:', result);
});
// Resolve from anywhere
setTimeout(() => {
deferred.resolve('Hello world!');
}, 1000);
// Or reject
deferred.reject(new Error('Something went wrong'));
// Or cancel
deferred.cancel('User canceled');🎯 Event-Based Resolution
function waitForUserInput() {
const deferred = defer<string>();
const button = document.getElementById('submit');
const input = document.getElementById('input') as HTMLInputElement;
button.addEventListener('click', () => {
deferred.resolve(input.value);
});
// Auto-cancel after 30 seconds
setTimeout(() => {
deferred.cancel('Timeout');
}, 30000);
return deferred.promise;
}
const userInput = await waitForUserInput();
console.log('User entered:', userInput);🔄 Managing Long-Running Operations
class TaskManager {
private currentTask: CancelablePromise<Result> | null = null;
async executeTask(task: Task) {
// Cancel previous task if running
this.currentTask?.cancel('New task started');
// Start new task
this.currentTask = api.performTask(task);
// Return new task promise
return this.currentTask;
}
cancel() {
this.currentTask?.cancel('User canceled');
}
}3️⃣ toCancelablePromise - Universal Converter
Convert anything to a CancelablePromise:
import { toCancelablePromise } from 'easy-cancelable-promise';
// From native Promise
const native = Promise.resolve('hello');
const cancelable = toCancelablePromise(native);
cancelable.cancel(); // Now cancelable!
// From value
const fromValue = toCancelablePromise(42);
console.log(await fromValue); // 42
// Already cancelable? Returns as-is
const alreadyCancelable = new CancelablePromise((resolve) => resolve('hi'));
const same = toCancelablePromise(alreadyCancelable);
console.log(same === alreadyCancelable); // true4️⃣ groupAsCancelablePromise - Concurrency Control
Group multiple promises with advanced control over execution:
import { groupAsCancelablePromise } from 'easy-cancelable-promise';
const tasks = [
() => fetchUser(1),
() => fetchUser(2),
() => fetchUser(3),
() => fetchUser(4),
() => fetchUser(5),
];
// Execute with concurrency limit
const group = groupAsCancelablePromise(tasks, {
maxConcurrent: 2, // Only 2 at a time
});
group.onProgress((percent) => {
console.log(`${percent}% complete`);
});
const results = await group;
console.log('All users:', results);
// Cancel all pending tasks
group.cancel('User navigated away');🎛️ Configuration Options
groupAsCancelablePromise(tasks, {
// Max concurrent executions (default: 8)
maxConcurrent: 3,
// Execute in order (default: false)
executeInOrder: true,
// Called before each task
beforeEachCallback: (index) => {
console.log(`Starting task ${index}`);
},
// Called after each success
afterEachCallback: (result, index) => {
console.log(`Task ${index} completed:`, result);
},
// Called when queue is empty
onQueueEmptyCallback: () => {
console.log('All tasks complete!');
},
});🎯 Real-World: Batch Processing
async function processBatch(items: Item[]) {
const tasks = items.map((item) => () => api.processAndSaveItem(item)); // returns CancelablePromise
return groupAsCancelablePromise(tasks, {
maxConcurrent: 5,
beforeEachCallback: (index) => {
updateProgress(`Processing item ${index + 1}/${items.length}`);
},
afterEachCallback: (result, index) => {
logSuccess(`Item ${index + 1} processed`);
},
});
}
const batch = processBatch(items);
// Track progress
batch.onProgress((percent) => {
progressBar.style.width = `${percent}%`;
});
// Cancel if user navigates away
window.addEventListener('beforeunload', () => {
batch.cancel('Page unloading');
});
const results = await batch;🔄 Sequential Execution
const tasks = [() => step1(), () => step2(), () => step3()];
const sequential = groupAsCancelablePromise(tasks, {
maxConcurrent: 1,
executeInOrder: true,
});
// Guaranteed to execute in order, one at a time
const results = await sequential;5️⃣ Type Guards
Runtime type checking utilities:
import { isPromise, isCancelablePromise } from 'easy-cancelable-promise';
// Check if value is a Promise
if (isPromise(value)) {
await value;
}
// Check if it's a CancelablePromise
if (isCancelablePromise(value)) {
value.cancel();
console.log(value.status);
}🎯 Real-World: Polymorphic Handling
function handleAsyncValue(value: unknown) {
// Already cancelable? Use full API
if (isCancelablePromise(value)) {
value.onProgress((p) => console.log(`${p}%`));
return value;
}
// Promise or value - convert to cancelable
return toCancelablePromise(value);
}🎓 Comparison with Other Solutions
| Feature | easy-cancelable-promise | Native Promise | bluebird | p-cancelable | | ----------------------------- | ----------------------- | -------------- | -------- | ------------ | | ✅ 100% Promise compatible | ✅ | ✅ | ✅ | ✅ | | ✅ Cancelation | ✅ | ❌ | ✅ | ✅ | | ✅ Progress tracking | ✅ | ❌ | ❌ | ❌ | | ✅ Status property | ✅ | ❌ | ❌ | ❌ | | ✅ Multiple cancel listeners | ✅ | ❌ | ❌ | ❌ | | ✅ Dynamic cleanup strategies | ✅ | ❌ | ❌ | ❌ | | ✅ Concurrency control | ✅ | ❌ | ✅ | ❌ | | ✅ TypeScript first | ✅ | ✅ | ⚠️ | ✅ | | ✅ Zero dependencies | ✅ | ✅ | ❌ | ✅ | | ✅ Bundle size | ~6KB | 0 | ~632KB | ~13KB |
🚀 Get Started Now
npm install easy-cancelable-promiseThen in your app:
import { CancelablePromise } from 'easy-cancelable-promise';
const task = new CancelablePromise((resolve, reject, { onCancel }) => {
const timeoutId = setTimeout(() => resolve('Done!'), 5000);
onCancel(() => clearTimeout(timeoutId));
});
task.cancel(); // That's it!Your promises, your control. 🎉
🌐 Related Projects
- easy-web-worker - Easy Web Workers with CancelablePromise support
📝 Contributing
We welcome contributions! If you have an idea for a new feature or improvement, please open an issue or submit a pull request.
📄 License
MIT License - see LICENSE for details.
