5

I want to add a cancel method to a subclass of the Promise built-in. Why does this not work?

class CancellablePromise extends Promise {
    constructor(executor) {
        let cancel = null
        super((resolve,reject) => {
            cancel = reject
            executor(resolve, reject)
        })
        this.cancel = cancel
    }
}

const p = new CancellablePromise((resolve) => setTimeout(resolve, 1000))
    .then(() => console.log('success'))
    .catch((err) => console.log('rejected', err))

p.cancel() // Uncaught exception

Is the answer todo with Symbol.species?

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
Ben Aston
  • 53,718
  • 65
  • 205
  • 331

2 Answers2

5

The problem is that then, catch, and finally create and return a new promise, and the new promise that they create and return doesn't have the cancel method on it.

To fix that, you'd have to override then so it copies cancel from the current instance to the new one:

class CancellablePromise extends Promise {
    constructor(executor) {
        let cancel = null;
        super((resolve,reject) => {
            cancel = reject;
            executor(resolve, reject);
        });
        this.cancel = cancel;
    }

    then(onFulfilled, onRejected) {
        const p = super.then(onFulfilled, onRejected);
        p.cancel = this.cancel;
        return p;
    }
}

const p = new CancellablePromise((resolve) => setTimeout(resolve, 1000))
    .then(() => console.log('success'))
    .catch((err) => console.log('rejected', err));

p.cancel();

You don't need to do catch or finally, they're both defined using calls to then (per specification).

I should note that there are a lot of nuances around cancellable promises that I haven't gotten into in detail. It may be worth a deep read of Domenic Denicola's old (now withdrawn) cancellable promises proposal and this article by Ben Lesh.

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • and not forgetting that not all "thenables" are necessarily instantiated from the `Promise` class. – Alnitak Oct 01 '20 at 12:59
  • 1
    @Alnitak - Indeed yes. My practice is to ensure that anything that may just be a thenable goes through `Promise.resolve` sooner rather than later. :-) – T.J. Crowder Oct 01 '20 at 13:01
  • 1
    Inheritance is so fraught with accidental complexity, it's a deceptively advanced feature is it not? – Ben Aston Oct 01 '20 at 13:05
1

In fact, the then method returns a new instance of the current promise class, so the p.cancel method will be defined on the returned promise, but it refers to the last promise in the chain (the catch chain). Canceling a promise is much more difficult than just rejecting the first promise in the chain. At the very least, you need to reject the deepest pending promise in the chain and clean up internal long term operations like requests, streams, setTimeout, etc. They should look something like this:

import CPromise from "c-promise2";

const delay= (ms, value)=>{
    return new CPromise((resolve, reject, {onCancel}) => {
        const timer = setTimeout(resolve, ms, value);    
        onCancel(() => {
            log(`clearTimeout`);
            clearTimeout(timer);
        })
    })
}

const p= delay(1000, 123).then(console.log);

p.cancel();
Dmitriy Mozgovoy
  • 1,419
  • 2
  • 8
  • 7