All questions

Cancellable Promise

Premium

Cancellable Promise

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.

Signature

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>;

Examples

// 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.

Notes

  • Cancel means reject — a cancelled wrapper rejects with an Error whose name is CancelError and whose message is canceled. It never resolves after a cancel.
  • First outcome wins — whichever happens first, the input settling or cancel(), decides the wrapper. Every later event is ignored.
  • No late leaks — after a cancel, a later rejection of the input must be swallowed, not surfaced as an unhandled rejection.
  • 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.
  • No timers — this is pure promise plumbing; you never need setTimeout.

FAQ

Why can't you truly cancel a native JavaScript promise?
A promise is only a handle to work that is already running; there is no built-in way to stop that work. cancellablePromise does not abort the underlying operation — it rejects the wrapper and ignores the original's settlement, so your code stops reacting. To tear down the work itself (like a fetch), pair it with an AbortController.
What happens if the input settles before you call cancel()?
The first outcome wins. If the input resolves or rejects first, the settled flag is already true, so a later cancel() returns immediately: it does not override the value, does not re-reject, and does not run onCancel.
Why swallow a late rejection instead of just ignoring it?
Ignoring means attaching no rejection handler, which lets the input's late rejection bubble up as an unhandled promise rejection — a console warning in the browser and a potential process crash in Node. Swallowing means attaching a rejection handler that runs and returns, which marks the rejection handled.

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.

Upgrade to Premium