skip to Main Content

I am trying to get a better understanding of the Nodejs event loop and have got to the different types of queues that are used under the covers. My question is related to the micro queue used for promises.

From everything I have read, my current understanding is that the micro queue used for promises works on a FIFO basis (first in first out). However I have a small piece of example code that doesn’t make sense to me following that principle.

const promise1 = Promise.resolve();
const promise2 = Promise.resolve();

promise2.then(function p2() {
  console.log('in promise2.then()')
})
promise1.then(function p1() {
  console.log('in promise1.then()')
})
console.log('end test')

So from my current (quite possibly flawed understanding) of the order of events that will take place here is

  1. promise1 resolves instantly causing its handler (p1) to be added to the micro queue
  2. promise2 resolves instantly causing its handler (p2) to be added to the micro queue
  3. console.log('end test') gets added to the stack, instantly popped resulting in the log being printed
  4. Now that the stack is empty the p1 handler is picked off the queue (FIFO) and added to the stack resulting in in promise1.then() being printed
  5. The p2 handler is picked off the queue and added to the stack resulting in in promise2.then() being printed

This would result in an output of

end test
in promise1.then()
in promise2.then()

However what I am actually getting is

end test
in promise2.then()
in promise1.then()

I’ve even tried to run it through a visualiser like this https://www.jsv9000.app/?code=Y29uc3QgcHJvbWlzZTEgPSBQcm9taXNlLnJlc29sdmUoKTsKY29uc3QgcHJvbWlzZTIgPSBQcm9taXNlLnJlc29sdmUoKTsKCnByb21pc2UyLnRoZW4oZnVuY3Rpb24gcDIoKSB7CiAgY29uc29sZS5sb2coJ2luIHByb21pc2UyLnRoZW4oKScpCn0pCnByb21pc2UxLnRoZW4oZnVuY3Rpb24gcDEoKSB7CiAgY29uc29sZS5sb2coJ2luIHByb21pc2UxLnRoZW4oKScpCn0pCmNvbnNvbGUubG9nKCdlbmQgdGVzdCcp

My understanding seems to hold up while the handlers are being added to the micro queue but then everything falls apart once it starts picking things off.

My question is how is the p2 handler being picked off the micro queue and run before the p1 handler if the microqueue is a FIFO system?

2

Answers


  1. When the promises in your example resolve, there are no handlers yet. The promises are simply immediately fulfilled with undefined, then assigned to their respective variables.

    Only after that, you are calling .then() on the promises and passing in the handlers. Since the promises are already fulfilled by then, the handlers immediately get scheduled – and since you are doing promise2.then(p2) before promise1.then(p1), p2 comes first in the queue and is executed first.

    Either way, it should not matter. This kind of asynchronous code does not appear in the wild, where every promise is resolved as soon as possible, and independent promise chains race against each other so their order is unpredictable. If you cared about the order, you’d make them depend on each other.

    Login or Signup to reply.
  2. According to the Promise documentation:

    Promise.resolve() resolves a promise, which is not the same as
    fulfilling or rejecting the promise

    If the promise is immediately in a resolved state, nothing is added to the microtask queue. The ECMAScript Language Specification in section 27.2.4.7 does not indicate that any microtask job is created as a result of calling Promise.resolve()

    Then, section 27.2.5.4.1 says that if the promise is already in a resolved state when .then is called,

    1. Else if promise.[[PromiseState]] is fulfilled, then c. Perform HostEnqueuePromiseJob

    This is essentially saying that a microtask (job) is created every single time .then is called on a resolved promise.

    We can demonstrate that it is the .then call which creates a microtask, by seeing what happens when we call .then twice on the same resolved promise:

    const promise1 = Promise.resolve();
    const promise2 = Promise.resolve();
    
    promise2.then(function p2() {
      console.log('in promise2.then()')
    })
    promise1.then(function p1() {
      console.log('in promise1.then()')
    })
    promise1.then(function p1() {
      console.log('in promise1.then() a second time')
    })
    
    console.log('this text should appear first')

    As the output indicates, the queue is a FIFO queue. The only confusion was that the microtasks are created by .then calls rather then by the Promise.resolve() calls, because the promises are already resolved.

    You can see it work in the originally expected order if the promises are only resolved after the .then calls:

    const refs = {};
    const promise1 = new Promise(resolve => refs.resolveP1 = resolve);
    const promise2 = new Promise(resolve => refs.resolveP2 = resolve);
    
    promise1.then(function p1() {
      console.log('in promise1.then()')
    })
    promise2.then(function p2() {
      console.log('in promise2.then()')
    })
    promise1.then(function p1() {
      console.log('in promise1.then() a second time')
    })
    
    refs.resolveP1();
    refs.resolveP2();
    
    console.log('this text should appear first')

    This time, when .then is called, the callback functions are added to the PromiseFulfillReactions list for the promise, and are not invoked until something causes the promise to become resolved:

    1. If promise.[[PromiseState]] is pending, then a. Append fulfillReaction to promise.[[PromiseFulfillReactions]].

    Note that in the output of the preceding example, the two P1 messages appear first, even though the .then calls are not in that order. This is because they are in the PromiseFulfillReactions list for P1, and all execute at the time that P1 is resolved.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search