Skip to main content
4 of 7
Wording

Extending native JavaScript Promise with cancellation support

I'm playing with Promise Extensions for JavaScript (prex) library and I like it a lot. Amongst other things, this library appears to be a prototype behind the current ECMA TC39 proposal for cancellation and it uses the familiar cancellation token approach, popularized by .NET Task API.

I want to extend the standard Promise class with cancellation support, similar to how it is implemented in Bluebird (i.e., with an optional oncancel callback) but using prex.CancellationToken.

Here is a draft that can be run with NodeJS:

const prex = require('prex');

class CancellablePromise extends Promise {
  constructor(executor, token) {
    if (!token) {
      // if no token supplied, just delegate to the parent class
      super(executor);
      return;
    }
    
    const observeCancellation = async () => {
      // prex.Deferred is similar to TaskCompletionSource in .NET
      const deferred = new prex.Deferred();

      executor(
        deferred.resolve, 
        deferred.reject, 
        cancelListener => 
          deferred.cancelListener = cancelListener);  

      const registration = token.register(() => {
        try {
          // capture the CancelError
          token.throwIfCancellationRequested();
        }
        catch (cancelError) {
          try {
            // the token cancellation callback is synchronous,
            // and so is the executor-provided cancelListener callback
            deferred.cancelListener && deferred.cancelListener(cancelError);
            // reject here if cancelListener has not resolved/rejected it
            deferred.reject(cancelError);
          }
          catch (error) {
            // in case cancelListener throws
            deferred.reject(error);
          }
        }
      });
    
      try {
        return await deferred.promise;
      }
      finally {
        registration.unregister();
      }
    };

    super((resolve, reject) => 
      observeCancellation().then(resolve, reject));
  }
}

// delayWithCancellation
function delayWithCancellation(timeoutMs, token) {
  console.log(`delayWithCancellation: ${timeoutMs}`);

  return new CancellablePromise((resolve, reject, setCancelListener) => {
    token.throwIfCancellationRequested();
    const id = setTimeout(resolve, timeoutMs);
    setCancelListener(e => clearTimeout(id));
  }, token);
}

// main
async function main() {
  const tokenSource = new prex.CancellationTokenSource();
  setTimeout(() => tokenSource.cancel(), 2000); // cancel after 1500ms

  const token = tokenSource.token;

  await delayWithCancellation(1000, token);
  console.log("successfully delayed."); // we should reach here

  await delayWithCancellation(1500, token);
  console.log("successfully delayed."); // we should not reach here
}

main().catch(error => console.log(error));