CancelablePromise
The class behind everything else. How it is created, what its callback receives, how its status changes and what happens in a chain.
A Promise with three additions
CancelablePromise extends the native Promise. It adds:
- a
statusyou can read at any time, cancelandonCancel, to stop it and to clean up after it,reportProgressandonProgress, to say how far the work is.
Nothing else changes. It can be awaited, chained and passed to any code that expects a promise.
The callback
The constructor takes the same callback as Promise, with a third parameter: the utilities of the promise.
import { CancelablePromise } from 'easy-cancelable-promise';
export const task = new CancelablePromise<string>((resolve, reject, utils) => {
// resolve and reject work as in a Promise
const timer = setTimeout(() => resolve('Done!'), 1000);
// onCancel: subscribe to the cancellation. Returns a function that removes the subscription
const unsubscribe = utils.onCancel((reason) => {
clearTimeout(timer);
console.log('Canceled because:', reason);
});
// reportProgress: tell the listeners how far the work is
utils.reportProgress(50, { step: 'halfway' });
// status, isPending, isCanceled: read the state of the promise
console.log(utils.status()); // 'pending'
console.log(utils.isPending()); // true
console.log(utils.isCanceled()); // false
// onProgress: listen to the progress from inside
utils.onProgress((percentage) => console.log(percentage));
// cancel: cancel the promise from inside
if (document.hidden) utils.cancel('The page is hidden');
void unsubscribe;
void reject;
});| Utility | What it does |
|---|---|
onCancel | Registers a listener for the cancellation. Returns a function to remove it |
cancel | Cancels the promise from inside |
reportProgress | Sends a percentage, and optional metadata, to the progress listeners |
onProgress | Registers a progress listener from inside. Returns a function to remove it |
status | Returns the current status |
isPending | true while the promise is not settled |
isCanceled | true after the promise was canceled |
Status
Every promise has one of four statuses, in the status property.
A promise leaves pending once, and never changes again.
| Status | Meaning |
|---|---|
pending | The work is in progress |
resolved | resolve was called with a value |
rejected | reject was called, or the callback threw |
canceled | cancel was called while it was pending |
import { CancelablePromise } from 'easy-cancelable-promise';
const wait = (milliseconds: number) =>
new CancelablePromise<void>((resolve, _reject, { onCancel }) => {
const timer = setTimeout(resolve, milliseconds);
onCancel(() => clearTimeout(timer));
});
export async function statuses() {
const resolved = wait(10);
const rejected = new CancelablePromise<void>((_resolve, reject) => reject(new Error('Failed')));
const canceled = wait(10_000);
canceled.cancel();
await rejected.catch(() => {});
await canceled.catch(() => {});
await resolved;
return [resolved.status, rejected.status, canceled.status]; // ['resolved', 'rejected', 'canceled']
}A canceled promise is a rejected promise for await, catch and then. The status is what tells a
cancellation from a failure.
A promise settles once
Like a native promise, only the first call counts. After a promise is resolved, rejected or canceled, later calls
to resolve, reject and cancel are ignored and the status does not change.
import { CancelablePromise, defer } from 'easy-cancelable-promise';
export async function settleOnce() {
const deferred = defer<string>();
deferred.resolve('first');
deferred.resolve('second'); // ignored
deferred.reject(new Error('late')); // ignored
deferred.cancel(); // ignored: a settled promise cannot be canceled
const value = await deferred.promise; // 'first'
// resolving with a promise: the status follows that promise
const outer = new CancelablePromise<string>((resolve) => {
resolve(new Promise((done) => setTimeout(() => done('later'), 10)));
});
const before = outer.status; // 'pending', it can still be canceled
await outer;
return [value, before, outer.status]; // ['first', 'pending', 'resolved']
}Resolving with another promise does not settle it yet. The promise stays pending, and can still be canceled,
until the promise you passed settles. Canceling it also cancels the promise you passed, when that one is a
CancelablePromise. A native promise cannot be canceled: its result is ignored.
When the callback throws
An error thrown by the callback rejects the promise, same as a native Promise.
import { CancelablePromise } from 'easy-cancelable-promise';
export async function executorThrows() {
const broken = new CancelablePromise<string>(() => {
throw new Error('Could not start');
});
// same as a Promise: the error becomes the rejection
const reason = await broken.catch((error) => (error as Error).message);
return [broken.status, reason]; // ['rejected', 'Could not start']
}The callback may be an async function. An error it throws, also after an await, rejects the promise the same
way.
Chains
then, catch and finally return a CancelablePromise. Canceling a promise of a chain cancels the promises
it was created from, so the cleanup of the first promise runs no matter which link is canceled.
import { CancelablePromise } from 'easy-cancelable-promise';
export function chain() {
let stopped = false;
const source = new CancelablePromise<number>((resolve, _reject, { onCancel }) => {
const timer = setTimeout(() => resolve(21), 5000);
onCancel(() => {
clearTimeout(timer);
stopped = true;
});
});
// then, catch and finally return a CancelablePromise
const doubled = source.then((value) => value * 2);
const text = doubled.then((value) => `The answer is ${value}`);
// canceling the last promise of a chain cancels every promise before it
text.cancel('Not needed');
return {
source: source.status, // 'canceled'
doubled: doubled.status, // 'canceled'
text: text.status, // 'canceled'
stopped, // true: the timer of the first promise was cleared
last: text.catch((reason) => reason), // resolves with 'Not needed'
};
}The cancellation travels up the chain, towards the source of the work.
It also travels down. When a promise is canceled, the promises chained from it are canceled too, with the same
reason. A handler that deals with the cancellation, as catch does, stops it there: that promise follows the
handler.
import { CancelablePromise } from 'easy-cancelable-promise';
export async function chainDown() {
const source = new CancelablePromise<number>(() => {});
// no handler for the cancellation: these promises are canceled with their source
const doubled = source.then((value) => value * 2);
const text = doubled.then((value) => `The answer is ${value}`);
// a handler that deals with the cancellation: this promise follows the handler
const recovered = source.catch(() => 0);
source.cancel('Not needed');
const value = await recovered; // 0
await text.catch(() => {});
return [doubled.status, text.status, recovered.status, value];
// ['canceled', 'canceled', 'resolved', 0]
}