Chor
Chor

Reputation: 981

How to explain the output order of this code snippet?

    new Promise((resolve,reject) => {
        console.log('outer promise')
        resolve()
    })
    .then(() => {
        console.log('outer 1 then')
        new Promise((resolve,reject) => {
            console.log('in promise')
            resolve()
        })
        .then(() => {
            console.log('in 1 then')
            return Promise.resolve()
        })
        .then(() => {
            console.log('in 2 then')
        })
    })
    .then(() => {
        console.log('outer 2 then')
    })
    .then(() => {
        console.log('outer 3 then')
    })
    .then(() => {
        console.log('outer 4 then')
    })  

Here are my explanation:

  1. Execute the new Promise, output outer promise

  2. Execute the first outer then, its callback go to the microtask queue. The second outer then, the third outer then and the fourth outer then execute later, but all of their callback don't go to the queue since each promise returned by each then still in pending state.

  3. Execute the callback of the first outer then, and output outer 1 then. After that, new Promise will output in promise

  4. Execute the first inner then, its callback go to the microtask queue. The callback of the second inner then don't go to the queue

  5. Now, the callback of the first outer then has totally finished, which means the promise returned by the first outer then has resolved. Thus, the callback of the second outer then go to the queue

  6. Execute the first task of the queue, it will output outer 2 then and makes the callback of the third then go to the queue. Next, execute the callback of the second inner then, it will output in 2 then

  7. Execute the callback of the third outer then, it will output outer 3 then and makes the callback of the fourth then go to the queue.

  8. At last, execute the callback of the fourth outer then, it will output outer 4 then

Therefore, the output order should be:

    outer promise
    outer 1 then
    in promise
    in 1 then
    outer 2 then
    in 2 then
    outer 3 then
    outer 4 then

But it actually output:

outer promise
outer 1 then
in promise
in 1 then
outer 2 then
outer 3 then
outer 4 then
in 2 then

What I am confused is why in 2 then will output at last? And why outer 2 then, outer 3 then, outer 4 then will output continuously? This is different from what I have thought about event loop. I thought the result is something to do with the statement return Promise.resolve(), but I don't know what the statement exactly do and why it will influenced the output order.

Upvotes: 2

Views: 293

Answers (2)

Yousaf
Yousaf

Reputation: 29282

Before explaining the output of your code, it is important to note that real-world code should not rely on the timing of promises in unrelated promise chains. If you want one promise to settle after/before another promise, instead of relying on the timing in which both of them will be resolved, create a promise chain that settles the promises in a sequential manner.

Having said that, the key to understanding the output of your code is to understand how the promise returned by the nested then() method

...
.then(() => {
  console.log('in 1 then')
  return Promise.resolve()
})
...

is resolved.

Following steps explain the output:

  1. As the executor function passed to the Promise constructor is called synchronously, first thing that gets logged on the console is 'outer promise'

    micro-task queue: [ ]
    
    console output:
    ---------------
    outer promise
    
  2. After the above console.log statement, resolve function is called. As a result, the newly created promise is resolved synchronously.

    This queues a job in the micro-task queue to execute the fulfilment handler passed to the then() method.

    micro-task queue: [ job('outer 1 then') ]
    
    console output:
    ---------------
    outer promise
    
  3. After synchronous execution of the script ends, javascript can start processing the micro-task queue.

    First job in the micro-task is dequeued and processed. As a result, 'outer 1 then' and 'in promise' are logged on the console.

    micro-task queue: [ ]
    
    console output:
    ---------------
    outer promise
    outer 1 then
    in promise
    

    As the nested promise is resolved as a result of resolve() function call, a job is enqueued in the micro-task queue to execute its fulfilment handler.

    ...
    .then(() => {
      console.log('in 1 then');
      return Promise.resolve();
    })
    ...
    
    micro-task queue: [ job('in 1 then') ]
    
    console output:
    ---------------
    outer promise
    outer 1 then
    in promise
    
  4. As the callback function of the outer 1st then() method ends, implicitly returning undefined leads to the fulfilment of the promise returned by the wrapper then() method. As a result, another job is enqueued in the micro-task queue to execute the outer 2nd then() method's callback function

    micro-task queue: [ job('in 1 then'), job('outer 2 then') ]
    
    console output:
    ---------------
    outer promise
    outer 1 then
    in promise
    
  5. Next job in the micro-task queue is dequeued and processed. 'in 1 then' is logged on the console and as the return value of the callback function is a promise, i.e. Promise.resolve(), the promise returned by the wrapper inner then() method is resolved to the promise returned by its callback function.

    Following code example demonstrates how the promise returned by the then() method is resolved to the promise returned by its callback function:

    const outerPromise = new Promise((resolveOuter, rejectOuter) => {
       const innerPromise = new Promise((resolveInner, rejectInner) => {
          resolveInner(); 
       });
    
       // resolve/reject functions of the outer promise are 
       // passed to the `then()` method of the inner promise 
       // as the fulfilment/rejection handlers respectively
       innerPromise.then(resolveOuter, rejectOuter);
    });
    

    This means that the outer promise (returned by the then() method) now depends on the inner promise (return by its callback function). A job is enqueued in the micro-task queue to resolve the outer promise to the inner promise.

    micro-task queue: [ job('outer 2 then'), job(resolve(outerPr, innerPr) ]
    
    console output:
    ---------------
    outer promise
    outer 1 then
    in promise
    in 1 then
    
  6. Next job in the micro-task queue is dequeued and processed. 'outer 2 then' is logged on the console.

    As the return value of the callback function is undefined, the promise returned by the wrapper outer second then() method is resolved. This enqueues a job in the micro-task queue to execute the fulfilment handler passed to the outer 3rd then() method.

    micro-task queue: [ job(resolve(outerPr, innerPr), job('outer 3 then') ]
    
    console output:
    ---------------
    outer promise
    outer 1 then
    in promise
    in 1 then
    outer 2 then
    
  7. Next job in the micro-task queue is dequeued and processed.

    As the inner promise is resolved, a job is enqueued in the micro-task queue to resolve the outer promise. (resolve function of the outer promise is registered as a fulfilment handler of the inner promise) (see step 5)

    micro-task queue: [ job('outer 3 then'), job(resolve(outerPr) ]
    
    console output:
    ---------------
    outer promise
    outer 1 then
    in promise
    in 1 then
    outer 2 then
    
  8. Next job in the micro-task queue is dequeued and processed. 'outer 3 then' is logged on the console. A job is enqueued in the micro-task queue to execute the fulfilment handler passed to the 4th outer then() method.

    micro-task queue: [ job(resolve(outerPr), job('outer 4 then') ]
    
    console output:
    ---------------
    outer promise
    outer 1 then
    in promise
    in 1 then
    outer 2 then
    outer 3 then
    
  9. Next job in the micro-task queue is dequeued and processed. This resolves the promise returned by the 1st inner then() method. As a result, a job is enqueued in the micro-task queue to execute the fulfilment handler passed to the 2d inner then() method.

    micro-task queue: [ job('outer 4 then'), job('in 2 then') ]
    
    console output:
    ---------------
    outer promise
    outer 1 then
    in promise
    in 1 then
    outer 2 then
    outer 3 then
    
  10. Finally, the last two jobs in the micro-task queue are dequeued and processed one after the other, logging 'outer 4 then' and 'in 2 then' on the console respectively.

    micro-task queue: [ ]
    
    console output:
    ---------------
    outer promise
    outer 1 then
    in promise
    in 1 then
    outer 2 then
    outer 3 then
    outer 4 then
    in 2 then
    

Upvotes: 2

3limin4t0r
3limin4t0r

Reputation: 21120

I would shove this in the corner undefined behaviour. You shouldn't rely on the internals of the JavaScript engine to execute then callbacks in a certain order. I can imagine two scenarios that suit the given code.

  1. The inner tasks should be run before the outer tasks. Here you should make sure you return the inner promise. You can then move the inner then calls and add them to the outer promise chain.

    new Promise((resolve,reject) => {
      console.log('outer promise');
      resolve();
    })
    .then(() => {
      console.log('outer 1 then');
      return new Promise((resolve,reject) => { // <- added a return statement
        console.log('in promise');
        resolve();
      });
    })
    .then(() => {
      console.log('in 1 then');
      return Promise.resolve();
    })
    .then(() => {
      console.log('in 2 then');
    })
    .then(() => {
      console.log('outer 2 then');
    })
    .then(() => {
      console.log('outer 3 then');
    })
    .then(() => {
      console.log('outer 4 then');
    });

  2. You want to split off a path for the inner promise and don't care that further then callbacks execute before or after this separate path. The execution order between the two different paths should not be assumed. However within a path you can control the order. In the example below in 2 then should always be executed after in 1 then. The order of in 1 then and outer 2 then should not be assumed.

    const splitPoint = new Promise((resolve,reject) => {
      console.log('outer promise');
      resolve();
    })
    .then(() => {
      console.log('outer 1 then');
      return new Promise((resolve,reject) => { // <- added a return statement
        console.log('in promise');
        resolve();
      });
    });
    
    const path1 = splitPoint.then(() => {
      console.log('in 1 then');
      return Promise.resolve();
    })
    .then(() => {
      console.log('in 2 then');
    });
    
    const path2 = splitPoint.then(() => {
      console.log('outer 2 then');
    })
    .then(() => {
      console.log('outer 3 then');
    })
    .then(() => {
      console.log('outer 4 then');
    });
    
    Promise.all([path1, path2]).then(() => {
      console.log('path1 and path2 finished');
    });

Upvotes: 0

Related Questions