What is the order of execution in JavaScript promises?
Asked Answered
N

3

53

I'd like to understand the execution order of the following snippet that uses JavaScript promises.

Promise.resolve('A')
  .then(function(a){console.log(2, a); return 'B';})
  .then(function(a){
     Promise.resolve('C')
       .then(function(a){console.log(7, a);})
       .then(function(a){console.log(8, a);});
     console.log(3, a);
     return a;})
  .then(function(a){
     Promise.resolve('D')
       .then(function(a){console.log(9, a);})
       .then(function(a){console.log(10, a);});
     console.log(4, a);})
  .then(function(a){
     console.log(5, a);});
console.log(1);
setTimeout(function(){console.log(6)},0);

The result is:

1
2 "A"
3 "B"
7 "C"
4 "B"
8 undefined
9 "D"
5 undefined
10 undefined
6

I'm curious about the execution order 1 2 3 7... not the values "A", "B"...

My understanding is that if a promise is resolved the then function is put in the browser event queue. So my expectation was 1 2 3 4 ...

Why isn't 1 2 3 4 ... the logged order?

Nyhagen answered 26/4, 2016 at 16:18 Comment(3)
Promises work by return value they're not magical :) If you don't return from a then it won't work. If you add return to add your functions it'll work as you expect. There are like 100 duplicates of this - will just wait for bergi or jfriend to point out a good one.Batt
"I'm curious about the execution order 1 2 3 7... not the values 'A', 'B'..." Then remove the extraneous information and code from the question.Benford
if you don't explicitly return from promise, it will return undefined implicitlySerena
A
129

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. With actual asynchronous operations, you do not have a determinate behavior with non-connected, independent promise chains. 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

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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 Analysis

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.

  1. How are pending .then() handlers prioritized vs. setTimeout() calls that are also pending.

  2. 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 actual async API calls other than specifying .then() handlers. If you had any async operation that actually took any real time to execute at the start of this promise chain, then your setTimeout() would execute before the .then() handler on the real async operation just because the real async operation takes actual time to execute. So, this is a bit of a contrived example and is not the usual design case for real code.

For the second question, I've seen some discussion that discusses how pending .then() handlers at different levels of nesting should be prioritized. I don't know if that discussion was ever resolved in a specification or not. I prefer to code in a way that that level of detail does not matter to me. If I care about the order of my async operations, then I link my promise chains to control the order and this level of implementation detail does not affect me in any way. If I don't care about the order, then I don't care about the order so again that level of implementation detail does not affect me. Even if this was in some specification, it seems like the type of detail that should not be trusted across many different implementations (different browsers, different promise engines) unless you had tested it everywhere you were going to run. So, I'd recommend not relying on a specific order of execution when you have unsynchronized promise chains.


You could make the order 100% determinate by just linking all your promise chains like this (returning inner promises so they are linked into the parent chain):

Promise.resolve('A').then(function (a) {
    console.log(2, a);
    return 'B';
}).then(function (a) {
    var p =  Promise.resolve('C').then(function (a) {
        console.log(7, a);
    }).then(function (a) {
        console.log(8, a);
    });
    console.log(3, a);
    // return this promise to chain to the parent promise
    return p;
}).then(function (a) {
    var p = Promise.resolve('D').then(function (a) {
        console.log(9, a);
    }).then(function (a) {
        console.log(10, a);
    });
    console.log(4, a);
    // return this promise to chain to the parent promise
    return p;
}).then(function (a) {
    console.log(5, a);
});

console.log(1);

setTimeout(function () {
    console.log(6)
}, 0);

This gives the following output in Chrome:

1
2 "A"
3 "B"
7 "C"
8 undefined
4 undefined
9 "D"
10 undefined
5 undefined
6

And, since the promise have all been chained together, the promise order is all defined by the code. The only thing left as an implementation detail is the timing of the setTimeout() which, as in your example, comes last, after all pending .then() handlers.

Edit:

Upon examination of the Promises/A+ specification, we find this:

2.2.4 onFulfilled or onRejected must not be called until the execution context stack contains only platform code. [3.1].

....

3.1 Here “platform code” means engine, environment, and promise implementation code. In practice, this requirement ensures that onFulfilled and onRejected execute asynchronously, after the event loop turn in which then is called, and with a fresh stack. This can be implemented with either a “macro-task” mechanism such as setTimeout or setImmediate, or with a “micro-task” mechanism such as MutationObserver or process.nextTick. Since the promise implementation is considered platform code, it may itself contain a task-scheduling queue or “trampoline” in which the handlers are called.

This says that .then() handlers must execute asynchronously after the call stack returns to platform code, but leaves it entirely to the implementation how exactly to do that whether it's done with a macro-task like setTimeout() or micro-task like process.nextTick(). So, per this specification, it is not determinate and should not be relied upon.

I find no information about macro-tasks, micro-tasks or the timing of promise .then() handlers in relation to setTimeout() in the ES6 specification. This is perhaps not surprising since setTimeout() itself is not part of the ES6 specification (it is a host environment function, not a language feature).

I haven't found any specifications to back this up, but the answers to this question Difference between microtask and macrotask within an event loop context explain how things tend to work in browsers with macro-tasks and micro-tasks.

FYI, if you want more info on micro-tasks and macro-tasks, here's an interesting reference article on the topic: Tasks, microtasks, queues and schedules.

Agency answered 26/4, 2016 at 23:44 Comment(16)
Added info about the timing of pending .then() handlers vs. setTimeout() from the Promises/A+ specification.Agency
"As far as I know, there is no guarantee which comes first...", it depends entirely on the implementation of the promises. setTimeout actually has a minimum timeout delay (something like 15ms IIRC) but Promises may use setImmediate, which would of course be added to the JS event loop before a timer set at the same time. Promise implementations often use setTimeout(fn, 0), which would then resolve in the order that their minimum timers expire in, which would happen in the order in which they're called.Soricine
Also, I should mention I didn't downvote. I think you've got an excellent answer.Soricine
@Soricine - So, just to be 100% clear, you're agreeing that there is no specification that determines the relative order of the setTimeout() - it depends upon the implementation. And, the specification even permits use of setTimeout() for scheduling .then() handlers which could change the order from what is seen here.Agency
yep, just elaborating including what I thought were relevant bits for that section.Soricine
Added Summary section to try to explain the major points without having to follow all the line-by-line analysis.Agency
@Agency Thanks, thanks a lot for the detailed explanations! It's really an enormous amount of work! Frankly speaking - I didn't expected to get such a detailed answer! Thanks again!Nyhagen
This answer is good, but a bit too long imo. Also it does contain too many "I don't know"s and repetions. To answer the idk's: There is indeed no callback order specified between multiple, independent chains. A+ only guarantees in the cases like p.then(a).then(b) and p.then(a); p.then(b) that a is called before b. For reference, see github.com/promises-aplus/promises-spec/issues/77 and github.com/promises-aplus/promises-spec/issues/92. Maybe you can condense your post a bit :-)Wold
@DonHatch - Done.Agency
Wow @Agency awesome answer. By the way, what about catches? How does it behave if there is an inner "unlinked" Promise that throws an error, does the parent catch it?Chargeable
There is nothing unusual about NOT returning anything inside .then callback. I find it a very strange advise. In JavaScript a function without explicit return will return undefined, which will work fine and there is no reason to return anything else if that behavior makes the code work correctly.Bordy
@DmitriZaitsev - The context here is that if you have other promises inside a .then() handler and you do NOT return them from the .then() handler, then they are a completely separate execution chain and there becomes no way to track when they are done or if they have an error from the outside world. They essentially become fire and forget which is useful every once in a while, but not the typical case and not what the OP was trying to do at all. Returning those nested promises from the .then() handler inserts them into the current promise chain.Agency
@DmitriZaitsev - Of course you can have a promise with an undefined resolved value, but I don't think that's what the OP was pursuing here.Agency
Thanks for explaining, I can see that you mean that .then is not waiting for the async threads inside its functions. It may have its uses though, e.g. if you want to encapsulate your thread rather than leak to the outside. This is no different than using normal callback based APIs inside your functions, so calling it a "bug" might be a stretch :)Bordy
fwiw the "specification to back this up" is here. Promise resolution is always a microtask (in browsers), setTimeout a "macrotask", and after each task a "microtask checkpoint" happens. So in browsers .then always runs before setTimeoutEuton
"Why the downvote?" because after just 9 lines of that wall of text there is already a completely wrong claim: "You do not have a determinate behavior". Didn't even gone further... OP never exists the microtask-checkpoint there is a clearly defined behavior.Eleonoreeleoptene
F
2

The browser's JavaScript engine has something called the "event loop". There is only one thread of JavaScript code running at a time. When a button is clicked or an AJAX request or anything else asynchronous completes, a new event is placed into the event loop. The browser executes these events one at a time.

What you're looking at here is that you run code that executes asynchronously. When the asynchronous code completes, it adds an appropriate event to the event loop. What order the events are added in depends on how long each asynchronous operation takes to complete.

That means that if you're using something like AJAX where you have no control over what order the requests will complete in, your promises can execute in a different order each time.

Franchot answered 26/4, 2016 at 17:3 Comment(1)
Actually, in most browsers, .then callbacks execute on a microtask queue that is emptied at the end of the current run-to-completion, before the main event loop turns.Temperate
E
2

The HTML event loop contains both various task queues and one microtask queue.

At the beginning of each event-loop's iteration, a new task will be taken from one of the task queues, that's what are colloquially called "macro-tasks".

The microtask-queue however isn't visited only once per event-loop iteration. It is visited every time the JS call-stack is emptied. This means that it can be visited numerous times during a single event-loop iteration (because all tasks executed in an event-loop iteration don't come from a task queue).

One more particularity of that microtask-queue is that microtasks that are queued while the queue is being dequeued will get executed right away in the same checkpoint, without letting the event-loop do anything else.

In your example, everything chained or inside the first Promise.resolve("A") is either synchronous, or queuing a new microtask, without anything actually queuing a (macro)task.
This means that when the Event Loop enters the microtask checkpoint to execute the first Promise reaction callback, it will not leave that microtask checkpoint until the last queued microtask has been executed.
So your timeout is quite irrelevant here, it will get executed after all these Promise reactions.

This being clarified, we can now walk through your code and replace every Promise reaction with the underlying queueMicrotask(callback) it will call. It is then quite clear what is the order of execution:

queueMicrotask(function(a) { // first callback
  console.log(2, a, 1);

  queueMicrotask(function(a) { // second callback
    // new branch
    queueMicrotask(function(a) { // third callback
      console.log(7, a, 3);
      queueMicrotask(function(a) { // fifth callback
        console.log(8, a, 5);
      });
    }.bind(null, "C"));

    // synchronous (in second callback)
    console.log(3, a, 2);

    //main branch
    queueMicrotask(function(a) { // fourth callback (same level as third, but called later)
      // new branch
      queueMicrotask(function(a) { // sixth callback
        console.log(9, a, 6);
        queueMicrotask(function(a) { // eighth callback
          console.log(10, a, 8);
        });
      }.bind(null, "D"));

      // synchronous
      console.log(4, a, 4);

      // main branch
      queueMicrotask(function(a) { // seventh callback
        console.log(5, a, 7);
      });
    }.bind(null, a))
  }.bind(null, "B"));
}.bind(null, "A"));

// synchronous
console.log(1);
// irrelevant
setTimeout(function() {
  console.log(6);
});

Or if we extract every callbacks outside of the chain:

function first(a) {
  console.log(2, a, 1);
  queueMicrotask(second.bind(null, "B"));
}
function second(a) {
  queueMicrotask(third.bind(null, "C"));
  console.log(3, a, 2);
  queueMicrotask(fourth.bind(null, a));
}
function third(a) {
  console.log(7, a, 3);
  queueMicrotask(fifth);
}
function fourth(a) {
  queueMicrotask(sixth.bind(null, "D"));
  console.log(4, a, 4);
  queueMicrotask(seventh);
}
function fifth(a) {
  console.log(8, a, 5);
};
function sixth(a) {
  console.log(9, a, 6);
  queueMicrotask(eighth);
}
function seventh(a) {
  console.log(5, a, 7);
}
function eighth(a) {
  console.log(10, a, 8);
}
queueMicrotask(first.bind(null, "A"));

Now I should note that dealing with already resolved (or immediately resolved) Promises is not something you should see every day, so beware that as soon as one of these Promise reactions is actually bound to an asynchronous task, the order won't be reliable anymore, moreover since different (macro)task queues may have different priorities defined by the UA.
However, I believe it is still important to understand how the microtask-queue works to avoid blocking the event-loop by expecting Promise.resolve() will let the event loop breath, it won't.

Eleonoreeleoptene answered 24/8, 2021 at 6:29 Comment(1)
Spectacular answer, esp. with the queueMicrotask calls. Very helpful when mixing immediately resolving async functions with async functions that actually perform tasks.Jowl

© 2022 - 2024 — McMap. All rights reserved.