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));
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?
A B
in the SO snippet (FireFox).
– PeterMader
Commented
Sep 25, 2017 at 14:56
A B
in the SO snippet (Chrome).
– shabs
Commented
Sep 25, 2017 at 14:58
B
then A
in FF.
– Andy
Commented
Sep 25, 2017 at 14:58
B A
is correct, and A B
is consequence of snippet runner inconsistency. But I can't prove it yet :)
– Andrey
Commented
Sep 25, 2017 at 14:59
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.
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);
});