Comments
First off, running promises inside of a .then()
handler and NOT returning those promises from the .then()
callback creates a completely new unattached promise sequence that is not synchronized with the parent promises in any way. Usually, this is a bug and, in fact, some promise engines actually warn when you do that because it is almost never the desired behavior. The only time one would ever want to do that is when you're doing some sort of fire and forget operation where you don't care about errors and you don't care about synchronizing with the rest of the world.
So, all your Promise.resolve()
promises inside of .then()
handlers create new Promise chains that run independently of the parent chain. You do not have a determinate behavior. It's kind of like launching four ajax calls in parallel. You don't know which one will complete first. Now, since all your code inside those Promise.resolve()
handlers happens to be synchronous (since this isn't real world code), then you might get consistent behavior, but that isn't the design point of promises so I wouldn't spend much time trying to figure out which Promise chain that runs synchronous code only is going to finish first. In the real world, it doesn't matter because if order matters, then you won't leave things to chance this way.
Summary
All .then()
handlers are called asynchronously after the current thread of execution finishes (as the Promises/A+ spec says, when the JS engine returns back to "platform code"). This is true even for promises that are resolved synchronously such as Promise.resolve().then(...)
. This is done for programming consistency so that a .then()
handler is consistently called asynchronously no matter whether the promise is resolved immediately or later. This prevents some timing bugs and makes it easier for the calling code to see consistent asynchronous execution.
There is no specification that determines the relative order of setTimeout()
vs. scheduled .then()
handlers if both are queued and ready to run. In your implementation, a pending .then()
handler is always run before a pending setTimeout()
, but the Promises/A+ spec specification says this is not determinate. It says that .then()
handlers can be scheduled a whole bunch of ways, some of which would run before pending setTimeout()
calls and some of which might run after pending setTimeout()
calls. For example, the Promises/A+ spec allows .then()
handlers to be scheduled with either setImmediate()
which would run before pending setTimeout()
calls or with setTimeout()
which would run after pending setTimeout()
calls. So, your code should not depend upon that order at all.
Multiple independent Promise chains do not have a predictable order of execution and you cannot rely on any particular order. It's like firing off four ajax calls in parallel where you don't know which one will complete first.
If order of execution is important, do not create a race that is dependent upon minute implementation details. Instead, link promise chains to force a particular execution order.
You generally do not want to create independent promise chains within a .then()
handler that are not returned from the handler. This is usually a bug except in rare cases of fire and forget without error handling.
Line By Line Analsysis
So, here's an analysis of your code. I added line numbers and cleaned up the indentation to make it easier to discuss:
1 Promise.resolve('A').then(function (a) {
2 console.log(2, a);
3 return 'B';
4 }).then(function (a) {
5 Promise.resolve('C').then(function (a) {
6 console.log(7, a);
7 }).then(function (a) {
8 console.log(8, a);
9 });
10 console.log(3, a);
11 return a;
12 }).then(function (a) {
13 Promise.resolve('D').then(function (a) {
14 console.log(9, a);
15 }).then(function (a) {
16 console.log(10, a);
17 });
18 console.log(4, a);
19 }).then(function (a) {
20 console.log(5, a);
21 });
22
23 console.log(1);
24
25 setTimeout(function () {
26 console.log(6)
27 }, 0);
Line 1 starts a promise chain and attached a .then()
handler to it. Since Promise.resolve()
resolves immediately, the Promise library will schedule the first .then()
handler to run after this thread of Javascript finishes. In Promises/A+ compatible promise libraries, all .then()
handlers are called asynchronously after the current thread of execution finishes and when JS goes back to the event loop. This means that any other synchronous code in this thread such as your console.log(1)
will run next which is what you see.
All the other .then()
handlers at the top level (lines 4, 12, 19) chain after the first one and will run only after the first one gets its turn. They are essentially queued at this point.
Since the setTimeout()
is also in this initial thread of execution, it is run and thus a timer is scheduled.
That is the end of the synchronous execution. Now, the JS engine starts running things that are scheduled in the event queue.
As far as I know, there is no guarantee which comes first a setTimeout(fn, 0)
or a .then()
handler that are both scheduled to run right after this thread of execution. .then()
handlers are considered "micro-tasks" so it does not surprise me that they run first before the setTimeout()
. But, if you need a particular order, then you should write code that guarantees an order rather than rely on this implementation detail.
Anyway, the .then()
handler defined on line 1 runs next. Thus you see the output 2 "A"
from that console.log(2, a)
.
Next, since the previous .then()
handler returned a plain value, that promise is considered resolved so the .then()
handler defined on line 4 runs. Here's where you're creating another independent promise chain and introducing a behavior that is usually a bug.
Line 5, creates a new Promise chain. It resolves that initial promise and then schedules two .then()
handlers to run when the current thread of execution is done. In that current thread of execution is the console.log(3, a)
on line 10 so that's why you see that next. Then, this thread of execution finishes and it goes back to the scheduler to see what to run next.
We now have several .then()
handlers in the queue waiting to run next. There's the one we just scheduled on line 5 and there's the next one in the higher level chain on line 12. If you had done this on line 5:
return Promise.resolve.then(...)
then you would have linked these promises together and they would be coordinated in sequence. But, by not returning the promise value, you started a whole new promise chain that is not coordinated with the outer, higher level promise. In your particular case, the promise scheduler decides to run the more deeply nested .then()
handler next. I don't honestly know if this is by specification, by convention or just an implementation detail of one promise engine vs. the other. I'd say that if the order is critical to you, then you should force an order by linking promises in a specific order rather than rely on who wins the race to run first.
Anyway, in your case, it's a scheduling race and the engine you are running decides to run the inner .then()
handler that's defined on line 5 next and thus you see the 7 "C"
specified on line 6. It then returns nothing so the resolved value of this promise becomes undefined
.
Back in the scheduler, it runs the .then()
handler on line 12. This is again a race between that .then()
handler and the one on line 7 which is also waiting to run. I don't know why it picks one over the other here other than to say it may be indeterminate or vary per promise engine because the order is not specified by the code. In any case, the .then()
handler in line 12 starts to run. That again creates a new independent or unsynchronized promise chain line the previous one. It schedules a .then()
handler again and then you get the 4 "B"
from the synchronous code in that .then()
handler. All synchronous code is done in that handler so now, it goes back to the scheduler for the next task.
Back in the scheduler, it decides to run the .then()
handler on line 7 and you get 8 undefined
. The promise there is undefined
because the previous .then()
handler in that chain did not return anything, thus its return value was undefined
, thus that is the resolved value of the promise chain at that point.
At this point, the output so far is:
1
2 "A"
3 "B"
7 "C"
4 "B"
8 undefined
Again, all synchronous code is done so it goes back to the scheduler again and it decides to run the .then()
handler defined on line 13. That runs and you get the output 9 "D"
and then it goes back to the scheduler again.
Consistent with the previously nested Promise.resolve()
chain, the the schedule chooses to run the next outer .then()
handler defined on line 19. It runs and you get the output 5 undefined
. It is again undefined
because the previous .then()
handler in that chain did not return a value, thus the resolved value of the promise was undefined
.
As this point, the output so far is:
1
2 "A"
3 "B"
7 "C"
4 "B"
8 undefined
9 "D"
5 undefined
At this point, there is only one .then()
handler scheduled to be run so it runs the one defined on line 15 and you get the output 10 undefined
next.
Then, lastly, the setTimeout()
gets to run and the final output is:
1
2 "A"
3 "B"
7 "C"
4 "B"
8 undefined
9 "D"
5 undefined
10 undefined
6
If one were to try to predict exactly the order this would run in, then there would be two main questions.
How are pending .then()
handlers prioritized vs. setTimeout()
calls that are also pending.
How does the promise engine decide to prioritize multiple .then()
handlers that are all waiting to run. Per your results with this code it is not FIFO.
For the first question, I don't know if this is per specification or just an implementation choice here in the promise engine/JS engine, but the implementation you reported on appears to prioritize all pending .then()
handlers before any setTimeout()
calls. Your case is a bit of an odd one because you have no actua