LeoShi
LeoShi

Reputation: 1847

How to understand this Promise execution order?

I don’t understand why this piece of code results in such an order? Could anyone elaborate on this? I thought Promises were like a FIFO queue, but the nested Promise functions seems a little bit unpredictable, or maybe using some other data structure?

new Promise(resolve => {
    resolve()
  })
  .then(() => {
    new Promise(resolve => {
        resolve()
      })
      .then(() => {
        console.log(1)
      })
      .then(() => {
        console.log(2)
      })
      .then(() => {
        console.log(3.1)
      })
  })
  .then(() => {
    console.log(1.1)
    new Promise((resolve => {
        resolve()
      }))
      .then(() => {
        new Promise(resolve => {
            resolve()
          })
          .then(() => {
            console.log(4)
          })
          .then(() => {
            console.log(6)
          })
      }).then(() => {
        console.log(5)
      })
  }).then(() => {
    console.log(3)
  })
console.log(0)

Output:

0
1
1.1
2
3
3.1
4
5
6

Upvotes: 5

Views: 1312

Answers (4)

trincot
trincot

Reputation: 350137

As the accepted answer didn't really hit the mark (see the comments there), and also another upvoted answer makes claims of an "unpredictable order", I provide here mine, in the hope to take away some of these misconceptions.

The execution order is not accidental or random. It is completely determined by the ECMAScript language specification: there is just one possible outcome.

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. Also, an immediately resolving promise can be written more concisely with Promise.resolve(): it has the same effect.

So here is the rewritten code:

const a = Promise.resolve();
const b = a.then(function a_then() {
    const c = Promise.resolve();
    const d = c.then(function c_then() { console.log(1);   });
    const e = d.then(function d_then() { console.log(2);   });
    const f = e.then(function e_then() { console.log(3.1); });
});
const g = b.then(function b_then() {
    console.log(1.1);
    const h = Promise.resolve();
    const i = h.then(function h_then() {
        const j = Promise.resolve();
        const k = j.then(function j_then() { console.log(4); });
        const l = k.then(function k_then() { console.log(6); });
    });
    const m = i.then(function i_then() { console.log(5); });
});
const n = g.then(function g_then() { console.log(3); });
console.log(0);

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 the then callback is attached, whichever happens last.

step callstack action promise state changes microtask queue (FIFO)
1 script a = Promise.resolve() a is fulfilled -
2 script b = a.then(a_then) b is pending a_then/b
3 script g = b.then(b_then) g is pending a_then/b
4 script n = g.then(g_then) n is pending a_then/b
5 script log(0) a_then/b
6 - check microtask queue a_then/b
7 a_then c = Promise.resolve() c is fulfilled -
8 a_then d = c.then(c_then) d is pending c_then/d
9 a_then e = d.then(d_then) e is pending c_then/d
10 a_then f = e.then(e_then) f is pending c_then/d
11 a_then return (implicit) b is fulfilled c_then/d, b_then/g
12 - check microtask queue c_then/d, b_then/g
13 c_then log(1) b_then/g
14 c_then return (implicit) d is fulfilled b_then/g, d_then/e
15 - check microtask queue b_then/g, d_then/e
16 b_then log(1.1) d_then/e
17 b_then h = Promise.resolve() h is fulfilled d_then/e
18 b_then i = h.then(h_then) i is pending d_then/e, h_then/i
19 b_then m = i.then(i_then) m is pending d_then/e, h_then/i
20 b_then return (implicit) g is fulfilled d_then/e, h_then/i, g_then/n
21 - check microtask queue d_then/e, h_then/i, g_then/n
22 d_then log(2) h_then/i, g_then/n
23 d_then return (implicit) e is fulfilled h_then/i, g_then/n, e_then/f
24 - check microtask queue h_then/i, g_then/n, e_then/f
25 h_then j = Promise.resolve() j is fulfilled g_then/n, e_then/f
26 h_then k = j.then(j_then) k is pending g_then/n, e_then/f, j_then/k
27 h_then l = k.then(k_then) l is pending g_then/n, e_then/f, j_then/k
28 h_then return (implicit) i is fulfilled g_then/n, e_then/f, j_then/k, i_then/m
29 - check microtask queue g_then/n, e_then/f, j_then/k, i_then/m
30 g_then log(3) e_then/f, j_then/k, i_then/m
31 g_then return (implicit) n is fulfilled e_then/f, j_then/k, i_then/m
32 - check microtask queue e_then/f, j_then/k, i_then/m
33 e_then log(3.1) j_then/k, i_then/m
34 e_then return (implicit) f is fulfilled j_then/k, i_then/m
35 - check microtask queue j_then/k, i_then/m
36 j_then log(4) i_then/m
37 j_then return (implicit) k is fulfilled i_then/m, k_then/l
38 - check microtask queue i_then/m, k_then/l
39 i_then log(5) k_then/l
40 i_then return (implicit) m is fulfilled k_then/l
41 - check microtask queue k_then/l
42 k_then log(6) -
43 k_then return (implicit) l is fulfilled -
44 - check microtask queue -

This table shows there is no randomness involved: it is mechanically determined which functions execute in which order, completely determining the output.

Some highlights from the above table:

  • Notice the difference in the effect on the microtask queue in steps 2 and 3. In step 2, the call of a.then(a_then) puts a_then in the queue, while b.then(b_then) does not have that effect on b_then. The reason for this difference is that in step 2 the promise a is fulfilled, while in step 3 the promise b is not yet 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 3 we need to look at step 11 where it gets fulfilled, and then the conditions are right to put the b_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 for f, l, m and n: their fulfillment doesn't have any effect on the microtask queue.

Upvotes: 1

EugenSunic
EugenSunic

Reputation: 13693

It results in unpredictable order because of un-existing returns in your code.

Adding return to your promises and you'll get comprehensible outputs and can easily track the promises execution.

Firstly, synchronous 0 is printed then the entire first promise block gets executed, like you said FIFO.

1,2, 3.1

After that the chaining thenable gets executed 1.1 After that the block 4,6 gets printed

following the chaining thenable which outputs 5 and at last, the last thenable prints number 3

Leaving us with 0,1,2, 3.1, 1.1, 4,6,5,3

new Promise(resolve => resolve())
  .then(() => {
    return new Promise(resolve => resolve())
      .then(() => console.log(1))
      .then(() => console.log(2))
      .then(() => console.log(3.1));
  })
  .then(() => {
    console.log(1.1);
    return new Promise((resolve => resolve()))
      .then(() => {
        return new Promise((resolve) => resolve())
          .then(() => console.log(4))
          .then(() => console.log(6))
      }).then(() => console.log(5))
  }).then(() => console.log(3))

console.log(0)

Upvotes: 4

Gibor
Gibor

Reputation: 1721

Promises are async. This means everytime you create a new promise- a new async operation starts.

What is async operation in JS? First you need to understand that JS operates on a single thread no matter what you do. So, to make it looks like its asynchronous- there is something called the "event loop" (took the link from comment to original post, tnx @Taki for the great source).

In general, the event loop stores all the async functions and "slips" in the actions between the main code actions. This is really over-simplified explanation, refer to the link to read more, but thats the gist of it.

So basically, there is no "FIFO" queue here- the async functions order is literally depends on stuff like your processor speed, your operating system, etc.

BUT- there is a way to make sure one async action does get performed only after another one finishes, and this is the .then clause. The thing is, it only assures the specific function inside the .then will be performed after the specific promise it was concatenated to, but it does not say anything about the order of it in regars to other async operations (promises) in the event loop. So for example in your code:

new Promise(resolve => {
    resolve() // PROMISE A
  })
  .then(() => {
    new Promise(resolve => {
        resolve() // PROMISE B
      })
      .then(() => {
        console.log(1) //PROMISE C
      })
      .then(() => {
        console.log(2)
      })
      .then(() => {
        console.log(3.1)
      })
  })
  .then(() => {
    console.log(1.1) // PROMISE D
    new Promise((resolve => {
        resolve()
      }))

I took part of it to explain:

so, Promise A resolves first. this assures that promise B will resolve now. here is when things gets complicated: since promise B is resolved, both promise C and D now get into event loop! why? because Promise A had 2 .then clauses, so when the first one ends- event loop takes the 2nd one which is promise D. but the first .then clause had also a .then clause of his own - promise C, which also enters the event loop.

THERE IS NO CONNECTION BETWEEN PROMISE D AND C! They could be performed in any order. keep that logic and you'll see how it works out for the rest of the promises, and also if you try to run it on different OS it might be that promises order will be different because of different implementations of the OS for the event loop.

Hope this helps you to understand a little.

DISCLAIMER: I have not much experience in JS, but promises really intrigued me so I did a deep research about it. I'm standing behind everything I wrote here, but if there are any corrections to my explanation I'd love to hear!

EDIT

The answer beneath me is also correct but with no explanation, so let me add to it: When you do not return anything inside a promise (or a .then clause, which also returns a promise), it will implicitly return a resolved promise with no value before going out of the promise, basically like adding a return new Promise.resolve() after teh console.log in promise C, for example. When its done like this, all the .then clauses coming after promise B will only enter the event loop after the previous one ended (e.g b ends, so C goes into loop, then the next .then and so on), but between them other promises or .then clauses (like promise D) can enter as well.

But, when you RETURN the promise that has the .then clauses chained to it- it makes sure the whole block of the promise + then clauses goes into event loop as one in order, so the .then clauses will also be performed in the order you wanted :)

tnx @Eugene Sunic for the addition!

Upvotes: 8

marzelin
marzelin

Reputation: 11600

It's FIFO and the execution looks like this:

main [4] logs: 0 // main code executed, one executor added to FIFO (4)
4 [8,18] // executor at line 4 runs, two executors added to FIFO (8, 18)
8 [18,11] logs: 1 // etc etc
18 [11,23,36] logs: 1.1
11 [23,36,14] logs: 2
23 [36,14,27,33] 
36 [14,27,33] logs: 3
14 [27,33] logs: 3.1
27 [33,30] logs: 4
33 [30] logs: 5
30 logs: 6

as you can see its first in first out order: [4,8,18,11,23,36,14,27,33,30] but it stores executors (callbacks for promises that were fulfilled or rejected), not promises. In other words: the time when promise is fulfilled or rejected decides when its added to FIFO not the time the promise is created.

Upvotes: 0

Related Questions