npm.io
3.0.1 • Published 1 week ago

easy-cancelable-promise

Licence
MIT
Version
3.0.1
Deps
0
Size
73 kB
Vulns
0
Weekly
0

easy-cancelable-promise

Image John Avatar

The cancelable promise you didn't know you needed.

Promises you can actually control.

npm version Downloads License

Documentation • Live examples • API reference • GitHub • NPM


The Problem

You're fetching user data, but the user navigated away. Your fetch continues anyway. Memory leak. Wasted bandwidth. Potential race conditions.

Native promises can't be canceled. Their status can't be tracked. Once started, they run to completion. Always.

The Solution

import { CancelablePromise } from 'easy-cancelable-promise';

const fetchUser = new CancelablePromise(
  async (resolve, reject, { onCancel }) => {
    const controller = new AbortController();

    onCancel(() => controller.abort());

    const user = await fetch('/api/user', { signal: controller.signal }).then(
      (res) => res.json(),
    );

    resolve(user);
  },
);

// User navigated? Just cancel it.
fetchUser.cancel('User navigated away');

Clean. Simple. The promise handles its own cleanup. Cancel from anywhere, anytime.

See it run: Stop the work. The same task in a native promise and in a CancelablePromise, with the steps each one still runs after you press Stop.


See It Before You Read It

Three live examples run the real library in your browser, next to the same code written with native promises. Each one shows measured numbers.

Example What you will see
Stop the work After Stop, the native promise runs every remaining step. The CancelablePromise runs none.
Progress through the promise One promise is silent until the end. The other reports every step, also through .then().
Cancel a group One cancel stops the running tasks and the queue. A queue of native promises starts them all.

Installation

npm install easy-cancelable-promise

Zero dependencies. TypeScript ready. Works everywhere.

Start here: Getting started


Why Developers Love This Library

100% Promise Compatible
// If you know this...
const native = new Promise((resolve, reject) => {
  // ...
});

// You know this!
const cancelable = new CancelablePromise((resolve, reject, { onCancel }) => {
  // ...
});

Works with async/await, .then(), .catch() - everything!

Docs: CancelablePromise


Lifecycle Control
const task = new CancelablePromise((resolve, reject, { onCancel }) => {
  const timer = setTimeout(() => resolve('Done!'), 5000);

  onCancel((reason) => {
    clearTimeout(timer);
    console.log('Cleaned up because:', reason);
  });
});

task.cancel('No longer needed'); // Cleanup happens automatically

Canceling tells everyone to stop waiting. onCancel is where the work itself is stopped.

Live example: Stop the work · Docs: cancel and onCancel


Built-in Progress Tracking
const download = new CancelablePromise(
  (resolve, reject, { reportProgress }) => {
    let received = 0;

    const timer = setInterval(() => {
      received += 25;

      // Report progress as you go
      reportProgress(received);

      if (received < 100) return;

      clearInterval(timer);
      resolve('Done!');
    }, 100);
  },
);

const result = await download.onProgress((percent) => {
  console.log(`${percent}% complete`); // 25%, 50%, 75%, 100%
});

console.log('Finished:', result);

The progress also reaches the promises chained with .then().

Live example: Progress through the promise · Docs: Progress reporting


Status Tracking
const task = new CancelablePromise((resolve) => setTimeout(resolve, 1000));

console.log(task.status); // 'pending'

await task;
console.log(task.status); // 'resolved'

const other = new CancelablePromise(() => {});

other.cancel();
console.log(other.status); // 'canceled'

Four statuses: pending, resolved, rejected and canceled. A cancellation is never confused with a failure.

Docs: Status


Chains That Cancel Together
const user = fetchUser(1); // a CancelablePromise
const name = user.then((value) => value.name);
const greeting = name.then((value) => `Hello ${value}`);

// Cancel any promise of the chain: the request is aborted
greeting.cancel('User navigated away');

console.log(user.status); // 'canceled'

then, catch and finally return a CancelablePromise. Canceling one cancels the promises it was created from, and the promises chained from a canceled promise are canceled too.

Docs: Chains


Utilities Included
import {
  defer,
  groupAsCancelablePromise,
  toCancelablePromise,
  CancelablePromise,
} from 'easy-cancelable-promise';

// Defer - external promise control
const deferred = defer<User>();
button.onclick = () => deferred.resolve(userData);

// Group - batch with concurrency, one cancel for all
const batch = groupAsCancelablePromise(
  [() => fetchUser(1), () => fetchUser(2), () => fetchUser(3)],
  { maxConcurrent: 2 },
);

// Static methods - just like Promise, and cancelable
const all = CancelablePromise.all([promise1, promise2]);
const race = CancelablePromise.race([promise1, promise2]);

// Convert - a native promise, a value or a function
const cancelable = toCancelablePromise(fetch('/api/user'));

Live example: Cancel a group · Docs: defer · Static helpers · Converting native promises


The API at a Glance

Export What it is Docs
CancelablePromise A Promise with status, cancel, onCancel, onProgress, reportProgress CancelablePromise
CancelablePromise.all Also allSettled, race, resolve, reject and canceled Static helpers
defer A promise with its resolve, reject and cancel outside the callback defer
groupAsCancelablePromise Many tasks, a concurrency limit, one promise Groups and concurrency
toCancelablePromise Converts a promise, a value or a function Converting native promises
isCancelablePromise Type guard for cancelable promises Type guards
isPromise Type guard for anything with a then method Type guards
CancelableAbortController An AbortController that tracks its subscriptions Abort signals
isCancelableAbortSignal Type guard for its signal Abort signals

Every signature and every exported type: API reference.


The Essentials

Executor Utilities

The third parameter gives you superpowers:

new CancelablePromise(
  (
    resolve,
    reject,
    { cancel, onCancel, reportProgress, status, isCanceled, isPending },
  ) => {
    // ✅ cancel: Cancel from inside
    cancel('Internal cancellation');

    // ✅ onCancel: Subscribe to cancellation
    const cleanup = onCancel((reason) => {
      console.log('Canceled:', reason);
    });

    // ✅ reportProgress: Report progress
    reportProgress(50); // 50% complete

    // ✅ status: Get current status
    console.log(status()); // 'pending'

    // ✅ isCanceled: Check if canceled
    if (isCanceled()) return;

    // ✅ isPending: Check if still pending
    if (isPending()) {
      // Continue work
    }
  },
);

Docs: The callback


Cancel a Real Request
import { CancelablePromise } from 'easy-cancelable-promise';

const fetchUser = (id: number) =>
  new CancelablePromise<User>(async (resolve, reject, { onCancel }) => {
    const controller = new AbortController();

    onCancel(() => controller.abort());

    const response = await fetch(`/api/users/${id}`, {
      signal: controller.signal,
    });

    resolve(await response.json());
  });

const request = fetchUser(1);

// Cancel anytime
request.cancel('User navigated away');

The callback can be async. An error it throws rejects the promise.

Docs: Getting started


Tell a Cancellation From a Failure
try {
  const user = await request;
} catch (error) {
  if (request.status === 'canceled') return; // nobody needs it any more

  showError(error);
}

The promise you cancel never causes an unhandled rejection, even when nothing catches it.

Docs: A cancellation is not a failure


Deferred Promises
import { defer } from 'easy-cancelable-promise';

function waitForUserInput() {
  const deferred = defer<string>();

  submitButton.addEventListener('click', () => deferred.resolve(input.value));

  // Auto-cancel after 30 seconds
  setTimeout(() => deferred.cancel('Timeout'), 30000);

  return deferred.promise;
}

const userInput = await waitForUserInput();

Docs: defer and DeferredPromise


Groups With a Concurrency Limit
import { groupAsCancelablePromise } from 'easy-cancelable-promise';

const batch = groupAsCancelablePromise(
  items.map((item) => () => api.processAndSaveItem(item)),
  {
    // Only 5 at a time (default: 8)
    maxConcurrent: 5,

    // Called before each task, with its position
    beforeEachCallback: (index) => {
      updateStatus(`Processing item ${index + 1}/${items.length}`);
    },

    // Called after each success, with the result and the position
    afterEachCallback: (result, index) => {
      logSuccess(`Item ${index + 1} processed`);
    },
  },
);

batch.onProgress((percent) => {
  progressBar.style.width = `${percent}%`;
});

// The results keep the order of the tasks
const results = await batch;

// One call cancels the running tasks and the ones still in the queue
batch.cancel('Page unloading');

Good to know:

  • If a task is rejected, the group is rejected with the same reason and no more tasks are started.
  • executeInOrder: true starts each task only after the previous one finished.
  • An empty list of tasks returns null.

Live example: Cancel a group · Docs: Groups and concurrency


Convert What You Already Have
import {
  toCancelablePromise,
  isCancelablePromise,
} from 'easy-cancelable-promise';

const fromPromise = toCancelablePromise(Promise.resolve('hello'));
const fromValue = toCancelablePromise(42);
const fromFunction = toCancelablePromise(() => Promise.resolve(true));

console.log(isCancelablePromise(fromPromise)); // true

A wrapped native promise can be canceled, but its work cannot be stopped: the native promise has no way to stop. To stop the work, create the CancelablePromise where the work starts.

Docs: What a wrapper can stop


Documentation

I want to... Go to
Create my first cancelable promise Getting started
Understand the class and its status CancelablePromise
Cancel, clean up and pass a reason cancel and onCancel
Report and track progress Progress reporting
Resolve a promise from outside defer and DeferredPromise
Use all, allSettled and race Static helpers
Wrap promises I already have Converting native promises
Limit concurrency and cancel a batch Groups and concurrency
Remove listeners with a signal Abort signals
Know the types and the package entries TypeScript
Test cancelable code Testing
Fix something that does not work Troubleshooting
Upgrade from version 2 Platform and versions

Version 3

  • Published as ES modules and CommonJS. Every entry of the package shares one class, so instanceof works between them.
  • A group rejects when a task fails, keeps the order of its results, and passes the position of each task to its callbacks.
  • A promise settles once: later calls to resolve, reject and cancel are ignored and the status does not change.
  • An error thrown by the callback, also an async one, rejects the promise.
  • The promises chained from a canceled promise are canceled too.

Everything that changed, and what to do about it: Upgrading from version 2.


Comparison with Other Solutions

Feature easy-cancelable-promise Native Promise bluebird p-cancelable
100% Promise compatible
Cancelation
Progress tracking
Status property
Multiple cancel listeners
Dynamic cleanup strategies
Concurrency control
TypeScript first
Zero dependencies

The whole library is about 7 KB minified and 2 KB gzipped, and bundlers drop what you do not import.



Contributing

We welcome contributions! If you have an idea for a new feature or improvement, please open an issue or submit a pull request.

yarn install
yarn test              # unit tests of the library and of the website
yarn dev               # the documentation site, against the built package
yarn prepare-packages  # everything: lint, types, tests, build, browser tests, links

License

MIT License - see LICENSE for details.


Keywords