A cancellable promise is a wrapper around an existing promise that can be told to stop caring about its result: once you call cancel(), the wrapper rejects with a dedicated cancellation error and quietly ignores whatever the original promise does next. Native promises have no cancel button — once you attach .then, you are committed to the outcome (see MDN on Promise). This wrapper gives you an escape hatch for the common case where the work is still running but you no longer want its result: a component unmounts, or a newer request supersedes an older one.
Implement cancellablePromise(promise, onCancel?), which returns { promise, cancel }. The returned promise mirrors the input. Calling cancel() before the input settles rejects the wrapper with an Error whose name is CancelError, and any later settlement of the input is ignored.
type Cancellable<T> = {
promise: Promise<T>; // mirrors the input — unless cancelled first
cancel: () => void; // reject with a CancelError; ignore later settlements
};
function cancellablePromise<T>(
promise: Promise<T>,
onCancel?: () => void, // optional cleanup — runs once, only on a real cancel
): Cancellable<T>;
// No cancel: the wrapper mirrors the input.
const cp = cancellablePromise(Promise.resolve('data'));
await cp.promise; // 'data'
// Cancel wins, and the input's later settlement is ignored.
const cp = cancellablePromise(slowRequest, () => console.log('cleanup'));
cp.cancel(); // logs 'cleanup'; the wrapper rejects now
try {
await cp.promise;
} catch (err) {
err.name; // 'CancelError'
err.message; // 'canceled'
}
// When slowRequest settles later, cp.promise stays rejected — nothing re-fires.
Error whose name is CancelError and whose message is canceled. It never resolves after a cancel.cancel(), decides the wrapper. Every later event is ignored.onCancel runs once, only on a real cancel — it fires exactly when cancel() cancels a still-pending wrapper. Not when the wrapper has already settled, and not on repeat cancel() calls.cancel() is idempotent — calling it again, or after the wrapper has settled, does nothing.setTimeout.Unlock the solution & editor
Runnable editor + tests
Solve in the browser with instant Jest feedback.
Detailed solutions
Walkthroughs, edge cases, and complexity notes.
Multi-framework variants
React, Vue, Vanilla, Angular — same question, different stacks.