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?
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:
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:
v => Promise.resolve("A")
v => "B"
p1 has v => console.log(v)
₁ in its list of fulfill reactions
v => console.log(v)
₂ in its list of fulfill reactionsWe 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:
v => "B"
Promise.resolve("A").then
with p1’s [[Resolve]] and [[Reject]]p1 has v => console.log(v)
₁ in its list of fulfill reactions
v => console.log(v)
₂ in its list of fulfill reactionsThe 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:
Promise.resolve("A").then
with p1’s [[Resolve]] and [[Reject]]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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With