0

Note:

This question has been closed as a duplicate, though I can’t see how it is the same as the referenced question.

Here I am asking about extending the Promise class. There the question is different, though it does mention a common goal of accessing the executor functions.


I am trying to extend JavaScript Promises using ES6 class syntax. In the extended class, I want to make the resolve and reject functions more accessible.

Here is a test script:

var executor = {};
var promise = new Promise((resolve,reject) => {
  executor = {resolve,reject};
});
promise.resolve = message => { executor.resolve(message ?? true); };
promise.reject = message => { executor.reject(message ?? false); };

document.querySelector('button#ok').onclick = event => promise.resolve('OK');
document.querySelector('button#cancel').onclick = event => promise.reject('Cancelled');

promise
.then(result => {console.log(result); })
.catch(error => {console.log(error); });
<button id="ok" type="button">OK</button>
<button id="cancel" type="button">Cancel</button>

Ultimately the code will be part of a pseudo dialog box.

As it stands, the Promise constructor stores the resolve and reject functions in an external variable, and two additional methods are bolted on to the resulting promise object.

I thought it should be s simple task to do this in an an inherited object:

class Promise2 extends Promise {
    constructor() {
        //  add the resolve and resolve functions as instance methods
    }
}

The problem is that the constructor needs to call super() to instantiate this and I can’t see how I can proceed from there.

Is it possible to extend Promise this way, or is there another way to store a reference to the resolve and reject functions in the object itself?

Manngo
  • 14,066
  • 10
  • 88
  • 110
  • 2
    You can make the resolve/reject functions more accessible with a Deferred object as shown here: https://stackoverflow.com/questions/37651780/why-does-the-promise-constructor-need-an-executor/37673534#37673534. – jfriend00 Oct 11 '22 at 03:28
  • @jfriend00 Are you the one who closed the question? I am asking a question about extending a class. The `Deferred` solution looks more like a factory function, which is OK, but the linked question is more about _why_?. Are you saying that it is not possible to extend the Promise class this way? – Manngo Oct 11 '22 at 03:39
  • FYI, you use the `Deferred` class just like you wanted to use your `Promise2` class. You do `const def = new Deferred()` and you can use `def.then(fn1)`, `def.catch(fn2)`, `def.finally(fn3)`, `def.resolve(val)` and `def.reject(e)` on it. – jfriend00 Oct 11 '22 at 07:08
  • 1
    I'm saying that the `Deferred` class solves the problem just fine. Is there something it doesn't do that you want/need? Subclassing promises can get messy and it doesn't appear that those complications are needed here. In this case, it's simpler to wrap a promise than to subclass it. – jfriend00 Oct 11 '22 at 07:19

2 Answers2

1

IMO, the simplest solution is to use the Deferred object that I referenced in the above comment and avoid sub-classing promises. There are complications when you subclass a promise because when someone calls .then() or .catch() on your sub-classed promise, those functions return a new promise and the system will try to make that new promise be your class. So your sub-class of the promise MUST fully support regular promise behavior in addition to your own. Debugging this stuff can be really, really confusing because your constructor gets called a lot more than you think it should because of all the system-created promises being constructed.

So, my recommendation is to use a Deferred as explained in this answer and it should fully support what you want to do without any of the complications of subclassing a promise.


If you really want to subclass a promise, you can do something like this:

class PromiseDeferred extends Promise {
    constructor(executor) {
        let res, rej;
        super((resolve, reject) => {
            res = resolve;
            rej = reject;
        });
        this.resolve = res;
        this.reject = rej;

        // when someone uses .then() or .catch() on our PromiseDeferred
        // that action creates a new promise and it will create one using
        // our class.  It will, however, call our constructor with a
        // regular executor function and expect it to work normally
        // so we have to support that behavior
        if (executor) {
            executor(res, rej);
        }
    }
}

// sample usage

let d1 = new PromiseDeferred();
d1.then(val => {
    console.log('Promise d1 resolved with', val);
}).catch(err => {
    console.log(`Promise d1 rejected with message: ${err.message}`);
});
d1.resolve("Hello");

// -----------------------------------------

let d2 = new PromiseDeferred();
d2.then(val => {
    console.log('Promise d2 resolved with', val);
}).catch(err => {
    console.log(`Promise d2 rejected with message: ${err.message}`);
});
d2.reject(new Error("Promise rejected"));

Performance?

FYI, another reason to avoid sub-classing promises is that you may lose a number of built-in optimizations when using those sub-classed promises. When the interpreter is dealing with a built-in object which it totally knows the behavior of, it can often make some performance optimizations that will be skipped if you subclass it. I know this is absolutely true if you subclass an Array. Some operations can be 20x slower on the sub-classed Array object. Since promises have had a lot of performance optimization done on them, it wouldn't surprise me if sub-classing them causes you to lose some of the performance optimizations too.

jfriend00
  • 683,504
  • 96
  • 985
  • 979
0

You can simply store the resolve/reject functions in the promise object itself to make them accessible. They are passed to the callback in the constructor, so you can instead pass your own callback there, save them, and then call the original callback. The issue with this not yet existing inside the callback can be handled by temporarily storing the functions in a local variable and only assigning them to this after the call to super returned:

class Promise2 extends Promise {
  constructor (callback) {
    let tmp
    
    super((resolve, reject) => {
      tmp = { resolve, reject }
      
      callback?.(resolve, reject)
    })
    
    Object.assign(this, tmp)
  }
}

(I used optional chaining on the function call to support omitting the callback argument too.)

CherryDT
  • 25,571
  • 5
  • 49
  • 74