I would like to know the reason why the execution order varies depending on what is returned in a then
callback:
var a = Promise.resolve();
var b = a.then(function a_then() {
console.log(1);
var c = Promise.resolve();
var d = c.then(function c_then() {
console.log(2);
});
var e = d.then(function d_then() {
console.log(3);
});
console.log(4);
return c; // <--- this influences the output order
});
var f = b.then(function b_then() {
console.log(5);
var g = Promise.resolve();
var h = g.then(function g_then() {
console.log(6);
});
var i = h.then(function h_then() {
console.log(7);
});
console.log(8);
});
console.log(9);
Without that return c
(so returning undefined
), the output order is: 9,1,4,2,5,8,3,6,7 and this order was explained as an answer to my previous question.
But adding that return c
changes the output order to 9,1,4,2,3,5,8,6,7.
Why is the value 3 now output before values 5 and 8? Why is the order of the events like this?
NB: A similar change in output happens when returning d
or e
.
It would be very nice if you could explain it using the table that was used as answer to my previous question:
Task | Action | a | b | c | d | e | f | g | h | i | PromiseJob queue |
---|---|---|---|---|---|---|---|---|---|---|---|
Script | a = Promise.resolve() |
F | - | - | - | - | - | - | - | - | |
Script | b = a.then(a_then) |
F | ? | - | - | - | - | - | - | - | a_then |
Script | f = b.then(b_then) |
F | ? | - | - | - | ? | - | - | - | a_then |
Script | console.log(9) |
F | ? | - | - | - | ? | - | - | - | a_then |
Host | dequeue a_then |
F | ? | - | - | - | ? | - | - | - | |
a_then |
console.log(1) |
F | ? | - | - | - | ? | - | - | - | |
a_then |
c = Promise.resolve() |
F | ? | F | - | - | ? | - | - | - | |
a_then |
d = c.then(c_then) |
F | ? | F | ? | - | ? | - | - | - | c_then |
a_then |
e = d.then(d_then) |
F | ? | F | ? | ? | ? | - | - | - | c_then |
a_then |
console.log(4) |
F | ? | F | ? | ? | ? | - | - | - | c_then |
a_then |
return resolves b |
F | F | F | ? | ? | ? | - | - | - | c_then , b_then |
Host | dequeue c_then |
F | F | F | ? | ? | ? | - | - | - | b_then |
c_then |
console.log(2) |
F | F | F | ? | ? | ? | - | - | - | b_then |
c_then |
return resolves d |
F | F | F | F | ? | ? | - | - | - | b_then , d_then |
Host | dequeue b_then |
F | F | F | F | ? | ? | - | - | - | d_then |
b_then |
console.log(5) |
F | F | F | F | ? | ? | - | - | - | d_then |
b_then |
g = Promise.resolve() |
F | F | F | F | ? | ? | F | - | - | d_then |
b_then |
h = g.then(g_then) |
F | F | F | F | ? | ? | F | ? | - | d_then , g_then |
b_then |
i = h.then(h_then) |
F | F | F | F | ? | ? | F | ? | ? | d_then , g_then |
b_then |
console.log(8) |
F | F | F | F | ? | ? | F | ? | ? | d_then , g_then |
b_then |
return resolves f |
F | F | F | F | ? | F | F | ? | ? | d_then , g_then |
Host | dequeue d_then |
F | F | F | F | ? | F | F | ? | ? | g_then |
d_then |
console.log(3) |
F | F | F | F | ? | F | F | ? | ? | g_then |
d_then |
return resolves e |
F | F | F | F | F | F | F | ? | ? | g_then |
Host | dequeue g_then |
F | F | F | F | F | F | F | ? | ? | |
g_then |
console.log(6) |
F | F | F | F | F | F | F | ? | ? | |
g_then |
return resolves h |
F | F | F | F | F | F | F | F | ? | h.then |
Host | dequeue h_then |
F | F | F | F | F | F | F | F | ? | |
h_then |
console.log(7) |
F | F | F | F | F | F | F | F | ? | |
h_then |
return resolves i |
F | F | F | F | F | F | F | F | F | |
Host | queue is empty | F | F | F | F | F | F | F | F | F |
When a
fulfills, the callback a_then
will be executed. The return value of that callback determines how b
resolves. If that then
callback returns a promise (like c
), a core feature of promises kicks in: the promise b
will not simply fulfill with that returned promise object, but will instead remain pending and get "locked-in" to the returned promise: the way promise c
will settle will determine how b
will settle -- b
's fate is locked-in to that of c
.
The Promise internal implementation that called a_then
makes this happen by inspecting the returned value. It detects that this returned value is a thenable (it has a then
method), and will actually plan to call that then
method (c.then
in this case) as that is the only way to get informed about the settled state of that thenable (c
in this case). It will pass its own (internal) callback to that then
method.
This is a callback that is not part of your JS code -- it is provided by the Promise implementation (see the ECMAScript specification on Promise resolve functions step 13). Moreover, this call of c.then
will happen asynchronously (see step 14 in the same specification). We can imagine that asynchronous job to look something like this (simplified):
function lockin_b(c) {
c.then(function fulfill_b(value) {
_fulfill(value)
});
}
...where _fulfill
is the internal function with which b
can be settled.
So, in summary, there are two extra asynchronous jobs involved in this locked-in scenario:
c.then
by the Promise internals so to lock-in b
to c
c.then
when c
is resolved.This means that b
will settle later than when that return c
would have been omitted, and consequently the b_then
callback that outputs 5 and 8 will also execute later.
Here is a sequence of events that happen during the execution of that code.
a
to i
represent the state of the promise with that name: ?
for pending, F
for fulfilled and R
for resolved, but not yet settled (i.e. locked-in).Task | Action | a | b | c | d | e | f | g | h | i | PromiseJob queue |
---|---|---|---|---|---|---|---|---|---|---|---|
Script | a = Promise.resolve() |
F | - | - | - | - | - | - | - | - | |
Script | b = a.then(a_then) |
F | ? | - | - | - | - | - | - | - | a_then |
Script | f = b.then(b_then) |
F | ? | - | - | - | ? | - | - | - | a_then |
Script | console.log(9) |
F | ? | - | - | - | ? | - | - | - | a_then |
Host | dequeue a_then |
F | ? | - | - | - | ? | - | - | - | |
a_then |
console.log(1) |
F | ? | - | - | - | ? | - | - | - | |
a_then |
c = Promise.resolve() |
F | ? | F | - | - | ? | - | - | - | |
a_then |
d = c.then(c_then) |
F | ? | F | ? | - | ? | - | - | - | c_then |
a_then |
e = d.then(d_then) |
F | ? | F | ? | ? | ? | - | - | - | c_then |
a_then |
console.log(4) |
F | ? | F | ? | ? | ? | - | - | - | c_then |
a_then |
return c resolves b |
F | R | F | ? | ? | ? | - | - | - | c_then , lockin_b |
Host | dequeue c_then |
F | R | F | ? | ? | ? | - | - | - | lockin_b |
c_then |
console.log(2) |
F | R | F | ? | ? | ? | - | - | - | lockin_b |
c_then |
return resolves d |
F | R | F | F | ? | ? | - | - | - | lockin_b , d_then |
Host | dequeue lockin_b |
F | R | F | F | ? | ? | - | - | - | d_then |
lockin_b |
c.then(fulfill_b) |
F | R | F | F | ? | ? | - | - | - | d_then , fulfill_b |
lockin_b |
return | F | R | F | F | ? | ? | - | - | - | d_then , fulfill_b |
Host | dequeue d_then |
F | R | F | F | ? | ? | - | - | - | fulfill_b |
d_then |
console.log(3) |
F | R | F | F | ? | ? | - | - | - | fulfill_b |
d_then |
return resolves e |
F | R | F | F | F | ? | - | - | - | fulfill_b |
Host | dequeue fulfull_b |
F | R | F | F | F | ? | - | - | - | |
fulfull_b |
_fulfill |
F | F | F | F | F | ? | - | - | - | b_then |
fulfull_b |
return | F | F | F | F | F | ? | - | - | - | b_then |
Host | dequeue b_then |
F | F | F | F | F | ? | - | - | - | |
b_then |
console.log(5) |
F | F | F | F | F | ? | - | - | - | |
b_then |
g = Promise.resolve() |
F | F | F | F | F | ? | F | - | - | |
b_then |
h = g.then(g_then) |
F | F | F | F | F | ? | F | ? | - | g_then |
b_then |
i = h.then(h_then) |
F | F | F | F | F | ? | F | ? | ? | g_then |
b_then |
console.log(8) |
F | F | F | F | F | ? | F | ? | ? | g_then |
b_then |
return resolves f |
F | F | F | F | F | F | F | ? | ? | g_then |
Host | dequeue g_then |
F | F | F | F | F | F | F | ? | ? | |
g_then |
console.log(6) |
F | F | F | F | F | F | F | ? | ? | |
g_then |
return resolves h |
F | F | F | F | F | F | F | F | ? | h.then |
Host | dequeue h_then |
F | F | F | F | F | F | F | F | ? | |
h_then |
console.log(7) |
F | F | F | F | F | F | F | F | ? | |
h_then |
return resolves i |
F | F | F | F | F | F | F | F | F | |
Host | queue is empty | F | F | F | F | F | F | F | F | F |
One can make a similar analysis for when the return
statement in the b_then
function returns a different promise. It is the same principle.