Skip to content
easy-cancelable-promise

CancelablePromise

The class behind everything else. How it is created, what its callback receives, how its status changes and what happens in a chain.

GuideTypeScriptTry an example

A Promise with three additions

CancelablePromise extends the native Promise. It adds:

  • a status you can read at any time,
  • cancel and onCancel, to stop it and to clean up after it,
  • reportProgress and onProgress, 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.

constructor.ts
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;
});
UtilityWhat it does
onCancelRegisters a listener for the cancellation. Returns a function to remove it
cancelCancels the promise from inside
reportProgressSends a percentage, and optional metadata, to the progress listeners
onProgressRegisters a progress listener from inside. Returns a function to remove it
statusReturns the current status
isPendingtrue while the promise is not settled
isCanceledtrue after the promise was canceled

Status

Every promise has one of four statuses, in the status property.

pending
resolved · rejected · canceled

A promise leaves pending once, and never changes again.

StatusMeaning
pendingThe work is in progress
resolvedresolve was called with a value
rejectedreject was called, or the callback threw
canceledcancel was called while it was pending
status.ts
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.

settle-once.ts
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.

executor-throws.ts
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.

chaining.ts
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.

chain-down.ts
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]
}

Next

Documentation
↑↓ Navigate ↵ Open Esc Closeeasy-cancelable-promise docs
Get started

Promises you can actually control.