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!
2
Answers
The difference is when your microtasks are added.
When JS calls this, it’s calling
.then()
on an already fulfilled promise, so it can add this handler to the microtask queue immediately as this line is executed (spec link), and so the callback will be executed as part of the next evaluation of the microtask queue.Promise.race()
doesn’t return a fulfilled promise immediately. Instead, it needs to handle the (first) fulfilment of the promises you pass it, and then in a subsequent tick resolve/reject itself based on this result, which then eventually results in your race-.then()
/.catch()
handlers from your code being called.You can think of race doing this internally for your case (spec link):
Internally the promise returned by
Promise.race()
is resolved bypromise1
/promise2
having.then()
callbacks attached to them, meaning that you have an additional step:.then()
handlers added internally by.race()
are queued as microtasks.race()
gets resolved, which then causes your.then((value) => { console.log('race:', value) })
handler to be added to the microtask queueIt is in between steps 1 and the subsequent tick required for step 2 above where
Promise.resolve().then(()=> {console.log('promise3')});
executes and is able add its handler onto the microtask queue, and that is why you seepromise3
logged first before therace:
log.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:
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 thethen
callback is attached, whichever happens last.log('script start')
new Promise(p1_construct)
p1_construct
log('promise1')
p1_construct
resolve("A")
p1 = <new promise>
p1
is fulfillednew Promise(p2_construct)
p2_construct
log('promise2')
p2_construct
resolve("A")
p2 = <new promise>
p2
is fulfilledp5 = Promise.race([p1, p2])
p5
is pending.race
registers then-callbacks. Queue.p1_then/p5
,p2_then/p5
p6 = p5.then(p5_then)
p6
is pending. Registerp5_then
p1_then/p5
,p2_then/p5
p7 = p6.catch(p6_catch)
p7
is pending. Registerp6_catch
p1_then/p5
,p2_then/p5
p3 = Promise.resolve()
p3
is fulfilledp1_then/p5
,p2_then/p5
p4 = p3.then(p3_then)
p4
is pending. Registerp3_then
. Queue.p1_then/p5
,p2_then/p5
,p3_then/p4
log('script end')
p1_then/p5
,p2_then/p5
,p3_then/p4
p1_then/p5
,p2_then/p5
,p3_then/p4
p1_then
p5
p5
is fulfilled. Queue.p2_then/p5
,p3_then/p4
,p5_then/p6
p2_then/p5
,p3_then/p4
,p5_then/p6
p2_then
p5
already resolved)p3_then/p4
,p5_then/p6
p3_then/p4
,p5_then/p6
p3_then
log('promise3')
p5_then/p6
p3_then
p4
p4
is fulfilled.p5_then/p6
p5_then/p6
p5_then
log('race:', value)
p5_then
p6
p6
is fulfilled.passthru/p7
passthru/p7
passthru
p7
p7
is fulfilled.Some highlights from the above table:
Notice the difference in the effect on the microtask queue in steps 11 and 14. In step 11, the call of
p5.then(p5_then)
does not put anything in the queue, while in step 14,p3.then(p3_then)
does append an entry in the microtask queue. The reason for this difference is that in step 11 the promisep5
is not yet fulfilled, while in step 14 the promisep3
is fulfilled. For a then-job to be added to the microtask queue we need the promise to be fulfilled. If it is not yet fulfilled, the callback will only be added to the queue when the promise fulfills later on. For the promise in step 11 we need to look at step 17 where it gets fulfilled, and then the conditions are right to put thep5_then
job in the queue.Some of the promises above don’t have any
then
callbacks attached to them, so when they fulfill, nothing much happens. This is the case forp4
,p7
: their fulfillment doesn’t have any effect on the microtask queue.As to
p5
(the promise returned byPromise.race
), it is pending when created. This is becauserace
can only know about the states of the promises (that it got as argument) by attachingthen
callbacks to them, which I have namedp1_then
andp2_then
(it also attaches catch callbacks, which I have ignored here). Thesethen
callbacks are callback functions thatrace
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 meansrace
will only know "mater" when a promise is resolved. Sorace
will always return a pending promise, no matter what the states of the promises are that are provided to it.