Saturday, 31 May 2025

Resolvable Promise

We know that JavaScript promises do not have a resolve or a reject method. A Promise constructor receives an executor function that represents the code to be run and that eventually will resolve or reject that Promise. For that, this executor function receives a resolve and reject function. So it's as if the Promise could only be resolved/rejected "from inside", by the code that "materializes" that Promise. Well, as I did in this old post, from inside the executor we can assign the resolve/reject function to some outer variable or field, and use that to resolve/reject the Promise "from outside".

With that in mind, I was thinking about having a resolvable (and rejectable) Promise that inherits from Promise and has a resolve and reject method (that just make use of the resolve/reject functions that were passed to the executor). Let's see how that can be implemented:


class ResolvablePromise extends Promise {
    constructor(executor) {
        let _resolve, _reject;
        let executorWrapper = (res, rej) => {
            _resolve = res;
            _reject = rej;
            executor(res, rej);
        };      
        super(executorWrapper);
        // now "this" is available
        this.resolve = _resolve;
        this.reject = _reject;  
    }
}

My initial implementation (see below) was failing (it was not throwing an exception as the error was captured by the Promise constructor itself), but I could see it with the debugger: Must call super constructor in derived class before accessing 'this' or returning from derived constructor"


class WRONGResolvablePromise extends Promise {
    // this does not work because "this" is not available yet in executorWrapper  
    // I see an "internal error": "Must call super constructor in derived class before accessing 'this' or returning from derived constructor"
    constructor(executor) {
        let executorWrapper = (res, rej) => {
            // here "this" (that the arrow function should find in the constructor through the Scope chain) is undefined
            this.resolve = res;
            this.reject = rej;
            executor(res, rej);
        };      
        super(executorWrapper);
    }

That's because in the constructor of a child class "this" is not available until super has been called. I was expecting that the arrow function that I use as executorWrapper would reach the lexical "this" in the ResolvablePromise.constructor through the Scope chain, but as I've said it's undefined there until super has finished

Now that we can freely resolve/reject a Promise, what happens if we try to resolve or reject again a promise that is no longer in pending state (it has already been resolved/rejected)? Nothing, the Promise state does not change and we don't get any error. This is different from Python asyncio, where invoking set_result or set_exception on a Future that is already done will cause an InvalidStateError exception.


let pr1 = new ResolvablePromise((resolve, reject) => {    
    setTimeout(() => {
        resolve('resolved');
    }, 2000);
});

pr1.resolve('manually resolved');
try {
    let res = await pr1;
    console.log(res);
} catch (err) {
    console.log(err);
}

let pr2 = pr1.then(res => {        
    console.log(res); //manually resolved
    return res;
});
console.log(pr2.constructor.name); // ResolvablePromise!!! rather than Promise

// I can resolve it again, it won't crash, it will just continue to be resolved to the same value
pr1.resolve('manually resolved again');
try {
    let res = await pr1;
    console.log(res); //manually resolved
} catch (err) {
    console.log(err);
}

// rejecting a resolved promise won't have any effect either
pr1.reject('manually rejected');
try {
    let res = await pr1;
    console.log(res); //manually resolved
} catch (err) {
    console.log(err);
}

/*
manually resolved
ResolvablePromise
manually resolved
manually resolved
manually resolved
*/


I thought that it would be important that the different methods in a ResolvablePromise (then, catch, finally) also returned a ResolvablePromise, rather than a standard Promise, so that we can apply the resolve/reject methods to the promises produced during chaining. I did an implementation and came across an odd problem, caused by the fact that in recent JavaScript versions that's no longer necessary. When subclassing a Promise the then-catch-finally methods of parent Promise already return an instance of the Child class! (see my "ResolvablePromise!!! rather than Promise" comment in the above code). This is an interesting topic that I'll explain in more detail in a separate post.

No comments:

Post a Comment