ES6 promise execution order for returned values

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.

Leave a Comment