Search code examples
javascriptpromiseevent-loop

how are chained promises queued in microtasks queue


(async function() {
    var a,b;

    function flush(){
        return new Promise(res => {
            res(123)
    })}

    Promise.resolve().then(() => a = 1)
    Promise.resolve().then(() => b = 2)

    await flush();

    console.log(a)
    console.log(b)

})()

In this snippet, the value of a and b gets logged in the console.

(async function() {
    var a;
    var b;

    function flush(){
        return new Promise(res => {
            res(123)
    })}

    Promise.resolve().then(() => a = 1).then(() => b = 2)   

    await flush();

    console.log(a)
    console.log(b)

})()

In this case the value of a gets logged as 1, whereas b is undefined.

(async function() {
    var a;
    var b;

    function flush(){
        return new Promise(res => {
            setTimeout(res)
    })}

    Promise.resolve().then(() => a = 1).then(() => b = 2)   

    await flush();

    console.log(a)
    console.log(b)

})()

This gives the same result as the first snippet, with the value a as 1 and b as 2

I would like to understand, why does promise chaining behaves differently than multiple individual promises

PS: I have a basic understanding of microtask queueing and the event loop.


Solution

  • Running Node 12.3.1, I can reproduce the observation stated in the question, after changing setTimeout(res(123)) to setTimeout(() => res(123)).

    In JavaScript the concurrency model is the event loop, in which the single thread executes callbacks from a queue.


    In the first snippet, the following happens.

    1. Since the promise is reoslved, .then adds the callback () => a = 1 to the queue.
    2. () => b = 2 is added to the queue.
    3. The code after the await1 () => console.log(a); console.log(b)2 is added to the queue.
    4. The callback in step 1 is run, a is set to 1
    5. b is set to 2
    6. a and b logged.

    Since setting the variables happens before printing them, you see both 1 and 2.


    In the second snippet:

    1. The callback () => a = 1 is added to the queue by .then
    2. The first .then returns a new promise, which is not resolved, as the first callback is not run yet. Then second .then attaches () => b = 2 to the pending promise.
    3. The code after the await () => console.log(a); console.log(b) is added to the queue.
    4. The callback () => a = 1 is run, and fulfills the promise created in step 2. That causes () => b = 2 to be added to the queue.
    5. a and b logged.
    6. b = 2 is run, but this happens after b, which was undefined, got printed.

    In Firefox however, the output of all three snippets are the same. I managed to produce the above behaviour by adding an async. Promise.resolve().then(async () => a = 1).then(() => b = 2)

    Here is a simplified that exhibits the same problem. 1 5 2 3 4 in Node but 1 2 3 5 4 in Firefox.

    (async function() {
      Promise.resolve()
        .then(() => console.log(1))
        .then(() => console.log(2))
        .then(() => console.log(3))
        .then(() => console.log(4))
      await Promise.resolve()
      console.log(5)
    })()
    

    But if you change the await to .then, Promise.resolve().then(() => console.log(5))

    You get 1 5 2 3 4 in both platforms.3

    Why? I googled and found this: https://v8.dev/blog/fast-async

    Node 12 optimizes away some extra steps with await, which previously required an extra throwaway promise, and two more microticks. That seems to be the reason "5" comes two steps earlier in Node 12.


    1. You can have the simplified mental model that await turns the rest of the code into a callback.
    2. In fact "the rest of the code" also resolves the promise created by the async function.
    3. Huh, so .then and await are different after all.