Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Extend Javascript promise and resolve or reject it inside constructor

Tags:

I want to extend native Javascript Promise class with ES6 syntax, and be able to call some asynchronous function inside the subclass constructor. Based on async function result the promise must be either rejected or resolved.

However, two strange things happen when then function is called:

  1. subclass constructor is executed twice
  2. "Uncaught TypeError: Promise resolve or reject function is not callable" error is thrown

    class MyPromise extends Promise {
        constructor(name) {
            super((resolve, reject) => {
                setTimeout(() => {
                    resolve(1)
                }, 1000)
            })

            this.name = name
        }
    }

    new MyPromise('p1')
        .then(result => {
            console.log('resolved, result: ', result)
        })
        .catch(err => {
            console.error('err: ', err)
        })
like image 700
humkins Avatar asked Jan 08 '18 22:01

humkins


People also ask

Can a promise resolve and reject?

A Promise that is resolved with the given value, or the promise passed as value, if the value was a promise object. It may be either fulfilled or rejected — for example, resolving a rejected promise will still result in a rejected promise.

What happens if you don't resolve a promise JavaScript?

A promise is just an object with properties in Javascript. There's no magic to it. So failing to resolve or reject a promise just fails to ever change the state from "pending" to anything else. This doesn't cause any fundamental problem in Javascript because a promise is just a regular Javascript object.

What happens if resolve () method is called twice inside the promise executor function?

The only thing to understand is that once resolved (or rejected), that is it for a defered object - it is done. If you call then(...) on its promise again, you immediately get the (first) resolved/rejected result. Additional calls to resolve() will not have any effect.


1 Answers

The reasoning is simple but not necessarily self evident.

  • .then() returns a promise
  • if then is called on a subclass of Promise, the returned promise is an instance of the subclass, not Promise itself.
  • the then returned promise is constructed by calling the subclass constructor, and passing it an internal executor function that records the value of resolve and reject arguments passed to it for later use.
  • "later use" covers resolving or rejecting the promise returned by then asynchronously when monitoring execution of onfulfilled or onrejected handlers (later) to see if they return a value (which resolves the then returned promise) or throw an error (which rejects the promise).

In short then calls internally obtain and record references to the resolve and reject functions of promises they return.


So regarding the question,
new MyPromise( 'p1')

works fine and is the first call to the subclass constructor.

.then( someFunction)

records someFunction in a list of then calls made on new MyPromise (recall then can be called multiple times) and attempts to create a return promise by calling

new MyPromise( (resolve, reject) => ... /* store resolve reject references */

This is the second call to the subclass constructor coming from then code. The constructor is expected to (and does) return synchronously.

On return from creating the promise to return, the .then method makes an integrity check to see if the resolve and reject functions it needs for later use are in fact functions. They should have been stored (in a list) along with callbacks provided in the then call.

In the case of MyPromise they are not. The executor passed by then, to MyPromise, is not even called. So then method code throws a type error "Promise resolve or reject function is not callable" - it has no means of resolving or rejecting the promise it is supposed to return.

When creating a subclass of Promise, the subclass constructor must take an executor function as its first argument, and call the executor with real resolve and reject functional arguments. This is internally required by then method code.

Doing something intricate with MyPromise, perhaps checking the first parameter to see if it is a function and calling it as an executor if it is, may be feasible but is outside the scope of this answer! For the code shown, writing a factory/library function may be simpler:

function namedDelay(name, delay=1000, value=1) {
     var promise = new Promise( (resolve,reject) => {
         setTimeout(() => {
                resolve(value)
            }, delay)
         }
     );
    promise.name = name;
    return promise;
}

namedDelay( 'p1')
    .then(result => {
        console.log('fulfilled, result: ', result)
    })
    .catch(err => {
        console.error('err: ', err)
    })

;TLDR

The class extension to Promise is not an extension. If it were it would need to implement the Promise interface and take an executor function as first parameter. You could use a factory function to return a Promise which is resolved asynchronously (as above), or hack the posted code with

MyPromise.prototype.constructor = Promise

which causes .then to return a regular Promise object. The hack itself refutes the idea that a class extension is taking place.


Promise Extension Example

The following example shows a basic Promise extension that adds properties supplied to the constructor. Of note:

  • The Symbol.toString getter only affects the output of converting an instance to a string. It does not change "Promise" to "MyPromise" when logging an instance object on browser consoles tested.

  • Firefox 89 (Proton) is not reporting own properties of extended instances while Chrome does - the reason test code below logs instance properties by name.

class MyPromise extends Promise {
    constructor(exec, props) {
        if( typeof exec != "function") {
            throw TypeError( "new MyPromise(executor, props): an executor function is required");
        }
        super((resolve, reject) => exec(resolve,reject));
        if( props) {
            Object.assign( this, props);
        } 
    }
    get [Symbol.toStringTag]() {
        return 'MyPromise';
    }
}

// Test the extension:
const p1 = new MyPromise( (resolve, reject) =>
    resolve(42),
    {id: "p1", bark: ()=>console.log("woof") });

console.log( "p1 is a %s object", p1.constructor.name);
console.log( "p1.toString() = %s", p1.toString());
console.log( "p1.id = '%s'", p1.id);
console.log( "p1 says:"); p1.bark();

const pThen = p1.then(data=>data);
console.log( "p1.then() returns a %s object", pThen.constructor.name);
let pAll = MyPromise.all([Promise.resolve(39)]);
console.log( "MyPromise.all returns a %s object", pAll.constructor.name);
try { new MyPromise(); }
catch(err) {
    console.log( "new MyPromise() threw: '%s'", err.message);
}
like image 187
traktor Avatar answered Sep 21 '22 08:09

traktor