Reputation: 1847
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
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
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
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
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