ES6 promise execution order for returned values

Trying to understand the order of execution of ES6 promises, I noticed the order of execution of chained handlers is affected by whether the previous handler returned a value or a promise.

Example

let a = Promise.resolve();
a.then(v => Promise.resolve("A")).then(v => console.log(v));
a.then(v => "B").then(v => console.log(v));

Output when run directly in Chrome (v 61) console:

B
A

However, when clicking the Run code snippet button, I will get the order A B instead.

Is the execution order defined in ES6 for the above example, or is it up to the implementation?

If it is defined, what should be the correct output?

Answers:

Answer

Promise.resolve is specified to return a resolved promise (mind-blowing, right? 25.4.4.5, 25.4.1.5, 25.4.1.3.). a.then() therefore enqueues a job immediately (25.4.5.3.1, step 8) each time. .then() never returns a fulfilled promise according to this spec (for something interesting, try Promise.resolve().then() in your Chrome console¹).

Let’s name the promise a.then(v => Promise.resolve("A")) and some of its associated spec state p1². This .then() enqueues a job to call (25.4.2.1) a.then(v => Promise.resolve("A")) as stated above.

The first .then(v => console.log(v)) appends a promise reaction corresponding to v => console.log(v)? to the list of fulfill reactions of the pending promise p1 (still 25.4.5.3.1).

  • The queue is now:

    1. fulfill reaction job v => Promise.resolve("A")
  • p1 now has v => console.log(v)? in its list of fulfill reactions

The promise a.then(v => "B") can be p2. It works in the same way for now.

  • The queue is now:

    1. fulfill reaction job v => Promise.resolve("A")
    2. fulfill reaction job v => "B"
  • p1 has v => console.log(v)? in its list of fulfill reactions

  • p2 now has v => console.log(v)? in its list of fulfill reactions

We have reached the end of the script.

When the first job, corresponding to v => Promise.resolve("A"), is dequeued and called (again 25.4.2.1), a then is found on the result (this is the important part), causing another job to be enqueued (25.4.1.3.2, step 12) regardless of the promise state of that result.

  • The queue is now:

    1. fulfill reaction job v => "B"
    2. call Promise.resolve("A").then with p1’s [[Resolve]] and [[Reject]]
  • p1 has v => console.log(v)? in its list of fulfill reactions

  • p2 has v => console.log(v)? in its list of fulfill reactions

The next job is dequeued and called. A callable then is not found on the result, so p2 is fulfilled immediately (25.4.1.3.2 again, step 11a) and enqueues a job for each of p2’s fulfill reactions.

  • The queue is now as follows:

    1. call Promise.resolve("A").then with p1’s [[Resolve]] and [[Reject]]
    2. call (via 25.4.2.1) v => console.log(v)?
  • p1 has v => console.log(v)? in its list of fulfill reactions

I’m going to stop this level of explanation here, as Promise.resolve("A").then starts the entire then sequence again. You can see where this is going, though: the job queue is a queue, and one function that’s going to produce output is in the queue and the other hasn’t yet been added. The one that’s in the queue is going to run first.

The correct output is B followed by A.

So, with that out of the way, why is the answer wrong in Chrome in a page by itself? It’s not some Stack Overflow snippets shim; you can reproduce it with a bit of HTML on its own or in Node. My guess is that it’s a spec-breaking optimization.

'use strict';

class Foo extends Promise {}

let a = Promise.resolve();
a.then(v => Foo.resolve("A")).then(v => console.log(v));
a.then(v => "B").then(v => console.log(v));

Alternate definitions of thenable with this fun node --allow_natives_syntax script!

'use strict';

const thenable = p => ({ then: p.then.bind(p) });
//const thenable = p => p;

let a = Promise.resolve();
a.then(v => {
    %EnqueueMicrotask(() => {
        %EnqueueMicrotask(() => {
            console.log("A should not have been logged yet");
        });
    });

    return thenable(Promise.resolve("A"));
}).then(v => console.log(v));
a.then(v => "B").then(v => console.log(v));

¹ For posterity: it’s a resolved promise in Chrome 61.0.3163.100.
² That’s less specific than the spec, but this is an answer trying to describe the spec and not a spec. With any luck, it’s even right, too.

Answer

Going strictly by the ES spec and its queue semantics, the order should be B A, as the additional promise in the first chain would take an additional microtask round. However this does prevent the usual optimisations, such as inspecting the resolution status and fulfillment value synchronously for known promises instead of inefficiently creating callbacks and going through then every time as mandated by the spec.

In any case, you should not write such code, or not rely on its order. You have two independent promise chains - a.then(v => Promise.resolve("A")) and a.then(v => "B"), and when each of those will resolve depends on what their callbacks do, which ideally is something asynchronous with unknown resolution anyway. The Promises/A+ spec does leave this open for implementations to handle synchronous callback functions either way. The golden rule of asynchronous programming in general, and with promises specifically, is to always be explicit about order if (and only if) you care about the order:

let p = Promise.resolve();
Promise.all([
  p.then(v => Promise.resolve("A")),
  p.then(v => "B")
]).then(([a, b]) => {
  console.log(a);
  console.log(b);
});

Tags

Recent Questions

Top Questions

Home Tags Terms of Service Privacy Policy DMCA Contact Us

©2020 All rights reserved.