Search code examples
javascriptpromisees6-promiseevent-loop

How to predict microtask order across Promise.then chains?


What is the JavaScript rule that determines the output of this code?:

Promise.resolve()
    .then(() => console.log("Microtask 1"))
    .then(() => console.log("Microtask 11"));

Promise.resolve()
    .then(() => console.log("Microtask 2"))
    .then(() => console.log("Microtask 22"))

This is the output:

Microtask 1
Microtask 2
Microtask 11
Microtask 22

But why isn't this the output?:

Microtask 1
Microtask 11
Microtask 2
Microtask 22

Solution

  • You see the output you see because the microtask for Microtask 11 isn't queued until the microtask for Microtask 1 runs, at which point the microtask for Microtask 2 is already in the queue waiting. So the order is (skipping some details; more below):

    1. Queue microtask for Microtask 1
    2. Queue microtask for Microtask 2
    3. Task is compelete, start processing microtasks:
      1. Execute microtask queued in #1
        • Output Microtask 1
        • Queue microtask for Microtask 11
      2. Execute microtask queued in #2
        • Output Microtask 2
        • Queue microtask for Microtask 21
      3. Execute microtask queued in #3.1
        • Output Microtask 11
      4. Execute microtask queued in #3.2
        • Output Microtask 21

    Thus,

    Microtask 1
    Microtask 2
    Microtask 11
    Microtask 21
    

    But: Writing code that relies on the order of execution of disconnected promise chains / microtask sequences is asking for trouble. :-) You can only reason about it in relation to promises you know are already settled, and of course in the normal case, you don't know when (or even if) a promise will settle. Instead, if you need a specific order, connect the chains to ensure order.


    Re skipped details: I sort of glossed over things in the main explanation to keep it short and clear, but for accuracy, let's look at:

    Promise.resolve()
        .then(() => console.log("Microtask 1"))
        .then(() => console.log("Microtask 11"));
    

    ...just on its own, ignoring the second promise chain to keep things simple.

    Here's how that code is executed (my blog post on promise terminology may be helpful while reading the below):

    1. Execute Promise.resolve(), creating a promise that in this particular case is fulfilled with the value undefined.
    2. Execute .then(() => console.log("Microtask 1")), creating a new function and calling then with it on the promise from Step 1. This creates and returns a new promise, and in this particular case, queues a microtask to call the function because the promise is already fulfilled.
    3. Execute .then(() => console.log("Microtask 11")), creating a new function and calling then with it on the promise from Step 2. That creates and returns a new promise (which we throw away in that code) and, since the promise from Step 2 is still pending, adds the function to it as a fulfillment handler.
    4. The task running this code ends, so the JavaScript engine processes the microtask queue:
      1. Execute the first microtask in the queue:
        1. Call the function logging Microtask 1.
        2. Since the function returns undefined, which isn't a promise or other thenable, the engine fulfills the promise from Step 2 with undefined. (If the function has returned a promise or other thenable, the promise would instead be resolved to the promise that was returned.)
          • That queues a microtask to call the fulfillment handler that was registered on that promise in Step 3, the one that will log Microtask 11 when called.
      2. Execute the next microtask in the queue (remember that we're ignoring the other promise chain in this description, so in this case the next microtask is the one logging Microtask 11 that was just added to the queue, not the one logging Microtask 2):
        1. Call the function logging Microtask 11.
        2. Since the function returns undefined, which isn't a promise or other thenable, the engine fulfills the promise from Step 3 with undefined.
          • That doesn't do anything, because that promise doesn't have any fulfillment handlers registered on it.