javascriptnode.jspromiseevent-loop

Sequence of promise.race() in the event loop


This code, below, executes as a result:

script start
promise1
promise2
script end
promise3
race: A

Chrome version 131.0.6778.26, and node environment 22.4 both give the same result

Why is it that “‘race’: A” is executed after the execution of promise3, my understanding is that Promise.race is also a microtask which is first put into the microtask stack, so it should be executed first, but the result is not like this, this problem has been bothering me for a long time. I hope to be able to give some official explanation, if you can answer my confusion I will be very grateful!

console.log('script start')

const promise1 = new Promise((resolve, reject) => {
    console.log('promise1')
    resolve("A")
})
const promise2 = new Promise((resolve, reject) => {
    console.log('promise2')
    resolve("B")
})

const p = Promise.race([promise1, promise2])
.then((value) => {
    console.log('race:', value)
}).catch((error) => {
    console.log(error)  
})

Promise.resolve().then(()=> {
    console.log('promise3')
})

console.log('script end')

I'm hoping for some official explanations, going down to the source code level would be best!


Solution

  • The execution order is not related to a particular V8 version. This is the only possible sequence you can get by the ECMAScript specification.

    Before making an analysis, I'll first rewrite the script in a way that every promise is assigned to a distinct variable, and every callback has a (function) name. That way we can uniquely reference all involved promises and functions.

    So here is the rewritten code:

    // All callback functions are defined with a name for easy reference in the analysis:
    function p1_construct(resolve) {
        console.log('promise1');
        resolve("A");
    }
    
    function p2_construct(resolve) {
        console.log('promise2');
        resolve("B");
    }
    
    function p3_then() {
        console.log('promise3');
    }
    
    function p5_then(value) {
        console.log('race:', value);
    }
    
    function p6_catch(error) {
        console.log(error);
    }
    
    console.log('script start');
    const p1 = new Promise(p1_construct);
    const p2 = new Promise(p2_construct);
    const p5 = Promise.race([p1, p2]);
    const p6 = p5.then(p5_then);
    const p7 = p6.catch(p6_catch);
    const p3 = Promise.resolve();
    const p4 = p3.then(p3_then);
    console.log('script end');

    The table below depicts the actions row by row as they are executed as time progresses.

    The "callstack" column indicates which function is executing (if any). "script" indicates the main script is executing (not a callback). Once the script has executed completely, the microtask queue will be checked for jobs, and if present the first job is extracted from it and executed. A promise-related job consists of executing a function and resolving the related promise with the function result. This pair of information (function and promise) is listed as a job entry in the last column, which represents the state of the microtask queue.

    If an action changes the state of a promise, this change is indicated in the "promise state changes" column.

    When a promise is fulfilled, and a then callback is attached to it, then the microtask queue gets a new job added to it. This happens either when the promise is fulfilled or the then callback is attached, whichever happens last.

    step callstack action promise state changes, assignments microtask queue (FIFO)
    1 script log('script start') -
    2 script new Promise(p1_construct) new promise is pending (but not yet assigned) -
    3 script > p1_construct log('promise1') -
    4 script > p1_construct resolve("A") new promise is fulfilled (but not yet assigned) -
    5 script p1 = <new promise> p1 is fulfilled -
    6 script new Promise(p2_construct) new promise is pending (but not yet assigned) -
    7 script > p2_construct log('promise2') -
    8 script > p2_construct resolve("A") new promise is fulfilled (but not yet assigned) -
    9 script p2 = <new promise> p2 is fulfilled -
    10 script p5 = Promise.race([p1, p2]) p5 is pending. race registers then-callbacks. Queue. p1_then/p5, p2_then/p5
    11 script p6 = p5.then(p5_then) p6 is pending. Register p5_then p1_then/p5, p2_then/p5
    12 script p7 = p6.catch(p6_catch) p7 is pending. Register p6_catch p1_then/p5, p2_then/p5
    13 script p3 = Promise.resolve() p3 is fulfilled p1_then/p5, p2_then/p5
    14 script p4 = p3.then(p3_then) p4 is pending. Register p3_then. Queue. p1_then/p5, p2_then/p5, p3_then/p4
    15 script log('script end') p1_then/p5, p2_then/p5, p3_then/p4
    16 - check microtask queue p1_then/p5, p2_then/p5, p3_then/p4
    17 p1_then resolve p5 p5 is fulfilled. Queue. p2_then/p5, p3_then/p4, p5_then/p6
    18 - check microtask queue p2_then/p5, p3_then/p4, p5_then/p6
    19 p2_then nothing (p5 already resolved) p3_then/p4, p5_then/p6
    20 - check microtask queue p3_then/p4, p5_then/p6
    21 p3_then log('promise3') p5_then/p6
    22 p3_then resolve p4 p4 is fulfilled. p5_then/p6
    23 - check microtask queue p5_then/p6
    24 p5_then log('race:', value) -
    25 p5_then resolve p6 p6 is fulfilled. passthru/p7
    26 - check microtask queue passthru/p7
    27 passthru resolve p7 p7 is fulfilled. -
    28 - check microtask queue -

    Some highlights from the above table:

    As to p5 (the promise returned by Promise.race), it is pending when created. This is because race can only know about the states of the promises (that it got as argument) by attaching then callbacks to them, which I have named p1_then and p2_then (it also attaches catch callbacks, which I have ignored here). These then callbacks are callback functions that race provides internally. So if one of the given promises is resolved, or will resolve, then at that moment the callback will be placed in the microtask queue (which we see in step 10). That means race will only know "later" when a promise is resolved. So race will always* return a pending promise, no matter what the states of the promises are that are provided to it.


    * Except when an error occurs as race processes the argument it received, in which case a rejected promise is returned. See ECMAScript specs