Search code examples
javascriptasynchronouspromisereturnorder-of-execution

The execution order of the promise according to the return type


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

Solution

  • 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:

    1. The asynchronous call of c.then by the Promise internals so to lock-in b to c
    2. The asynchronous call of the callback passed to 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.

    • The first column represents what is executing (the main script, a function initiated from the event loop, or the host that dequeues an item from the PromiseJob queue)
    • The second column has the current expression/statement being evaluated
    • The columns 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).
    • The last column pictures what is present in the PromiseJob queue, managed by the host
    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.