Skip to content
easy-cancelable-promise
Interactive example · Cancellation

Stop waiting, and stop the work too

The same task runs in a native Promise and in a CancelablePromise. Press Stop and count the steps each one still runs.

Try it hereLive example
Application preview

Loading the example…

search.ts
import { CancelablePromise } from 'easy-cancelable-promise';
import { primesInStep, type SearchOptions } from './work';

/**
 * The search in a native promise. Once it starts nothing can stop it:
 * whoever asked for the result can stop listening, but every step still runs.
 */
export function searchWithNativePromise({ from = 0, steps, pause, onStep }: SearchOptions): Promise<number> {
  return new Promise<number>((resolve) => {
    let step = 0;
    let found = 0;

    const next = () => {
      found += primesInStep(from + step);
      step += 1;

      onStep?.({ step, steps, found });

      if (step === steps) return resolve(found);

      setTimeout(next, pause);
    };

    setTimeout(next, pause);
  });
}

/**
 * The same search in a CancelablePromise. The promise owns its cleanup:
 * canceling it clears the timer, so the next step never runs. It also reports its progress.
 */
export function searchWithCancelablePromise({ from = 0, steps, pause, onStep }: SearchOptions): CancelablePromise<number> {
  return new CancelablePromise<number>((resolve, _reject, { onCancel, reportProgress }) => {
    let step = 0;
    let found = 0;
    let timer: ReturnType<typeof setTimeout>;

    onCancel(() => clearTimeout(timer));

    const next = () => {
      found += primesInStep(from + step);
      step += 1;

      onStep?.({ step, steps, found });
      reportProgress((step / steps) * 100, { step, steps, found });

      if (step === steps) return resolve(found);

      timer = setTimeout(next, pause);
    };

    timer = setTimeout(next, pause);
  });
}
Ready. Run the example to begin.Reset returns this example to its initial state.
Take it with you

A native promise can only be ignored. A CancelablePromise is told to stop, and it stops its own work.

01

Press Start both, then Stop both while the bars move. Both sides stop reporting to you at that moment.

02

Look at Steps after Stop. The native promise keeps running every remaining step. The CancelablePromise runs none.

03

Let it run to the end without pressing Stop. Both resolve with the same number of primes, so the work is the same.

Read the full guide

The code

Both functions do the same work: they count prime numbers in steps, with a pause between steps so the page stays free. The only difference is the promise they return.

search.ts
import { CancelablePromise } from 'easy-cancelable-promise';
import { primesInStep, type SearchOptions } from './work';

/**
 * The search in a native promise. Once it starts nothing can stop it:
 * whoever asked for the result can stop listening, but every step still runs.
 */
export function searchWithNativePromise({ from = 0, steps, pause, onStep }: SearchOptions): Promise<number> {
  return new Promise<number>((resolve) => {
    let step = 0;
    let found = 0;

    const next = () => {
      found += primesInStep(from + step);
      step += 1;

      onStep?.({ step, steps, found });

      if (step === steps) return resolve(found);

      setTimeout(next, pause);
    };

    setTimeout(next, pause);
  });
}

/**
 * The same search in a CancelablePromise. The promise owns its cleanup:
 * canceling it clears the timer, so the next step never runs. It also reports its progress.
 */
export function searchWithCancelablePromise({ from = 0, steps, pause, onStep }: SearchOptions): CancelablePromise<number> {
  return new CancelablePromise<number>((resolve, _reject, { onCancel, reportProgress }) => {
    let step = 0;
    let found = 0;
    let timer: ReturnType<typeof setTimeout>;

    onCancel(() => clearTimeout(timer));

    const next = () => {
      found += primesInStep(from + step);
      step += 1;

      onStep?.({ step, steps, found });
      reportProgress((step / steps) * 100, { step, steps, found });

      if (step === steps) return resolve(found);

      timer = setTimeout(next, pause);
    };

    timer = setTimeout(next, pause);
  });
}

The native version has nothing to call. The example can stop listening to it, but the timer that schedules the next step is out of reach, so every step still runs.

The cancelable version registers one line with onCancel. Canceling the promise clears the timer, and the next step never happens.

Related documentation

Keep exploring

Another piece of the model.

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

Promises you can actually control.