1

I am trying to understand the ordering of the following code (which I am running locally with node).

async function bfunc() {
  console.log("2 - b func");
}

async function afunc() {
  await bfunc();                           // rest of function pushed to event queue
  console.log('4 - a func');
}

const prom = new Promise((resolve, reject) => {
  console.log("1 - inside promise");
  resolve('5 - foo');
  afunc();
}).then((value) => {                       // this is also pushed to the event queue
  console.log(value);
});

console.log("3 - last thing");

This prints out:

1 - inside promise
2 - b func
3 - last thing
4 - a func
5 - foo

I believe most of my confusion stems from uncertainty around when events are pushed to the event queue.

  1. Are the 2 places commented above discerning events being pushed correct?

  2. In the case of the await call, is it the async function itself (including its stack environment that is pushed to the event queue?

  3. Why does '5 - foo' not print before '4 - a func'? I read here that

A pending promise can either be fulfilled with a value or rejected with a reason (error). When either of these options happens, the associated handlers queued up by a promise's then method are called

so I thought this would make the first event on the queue the handler of the then function.

ggorlen
  • 44,755
  • 7
  • 76
  • 106
shafe
  • 53
  • 1
  • 6
  • "Are the 2 places commented above discerning events being pushed correct?" -- I'm not sure I understand what this means. "Correct" by what definition? – ggorlen Jun 06 '21 at 21:27
  • 1
    You're not going to learn much useful here since NONE of these operations are actually asynchronous. And, you're far oversimplifying things to characterize something as "pushed to the event queue". There are multiple, separate queues within the event loop for different types of events. But, `await` or `.then()` are always resolved AFTER the current chain of Javascript finishes executing (as per the promise specification) which is why the last two lines of your output come last. – jfriend00 Jun 06 '21 at 21:29
  • @ggorlen, I suppose I mean correct as in accurate according to the language specification. ie. at those points in the code, events get pushed so that they'll ultimately be executed after the current chain. – shafe Jun 06 '21 at 21:52
  • @jfriend00 are the way these queues are implemented part of the javascript specification or are they engine specific? And in regard to “await or .then() always resolved AFTER the current chain”, so is that to say that everything that happens after the “await bfunc()” line (within afunc) will happen after the current chain? And lastly, can you explain why “2 - b func” is printed before “3 -last thing”? – shafe Jun 06 '21 at 21:52
  • See [this](https://stackoverflow.com/a/62405924/1048572) and the threads linked from there. Notice that the `afunc()` and the `prom` form completely independent promise chains, you should not try to argue about any ordering between them. – Bergi Jun 06 '21 at 23:25
  • 1 - yes. 2 - yes. 3 - that quote is misleading. When the promise is fulfilled, the handlers are not called immediately; rather, the handlers that were registered so fare are *scheduled* on the queue. Also notice that in your example, the promise is fullfilled immediately in the executor, even *before* the `.then()` method is invoked. It will then schedule the callback to be run asynchronously - as your comment on that line indicates. – Bergi Jun 06 '21 at 23:26

1 Answers1

3

Here's the punch line: Promises notify listeners of resolve/reject only AFTER the current sequence of Javascript finishes executing and returns control back to the event system. Read on for more detail on what that means and how it affects your code.

Let's put line numbers on your code so we can talk about more easily:

1     async function bfunc() {
2       console.log("2 - b func");
3     }
4    
5     async function afunc() {
6       await bfunc();         // rest of function pushed to event queue
7       console.log('4 - a func');
8     }
9    
10    const prom = new Promise((resolve, reject) => {
11      console.log("1 - inside promise");
12      resolve('5 - foo');
13      afunc();
14    }).then((value) => {     // this is also pushed to the event queue
15      console.log(value);
16    });
17    
18    console.log("3 - last thing");

Sequence of Events

  1. Line 10 executes. This causes const promise = new Promise(...).then(...) to begin to execute
  2. As part of the new Promise(...) constructor, it calls the executor function of the promise (the callback you passed to the constructor). That executes immediately and synchronously.
  3. As the promise executor is called synchronously, you see the first line of output 1 - inside promise from executing line 11.
  4. Then line 12 executes and the promise's state is changed to fulfilled with the value '5 - foo'.
  5. Then line 13 executes and calls afunc().
  6. Even though afunc() is declared async, it still begins to execute its function body synchronously so it immediately calls await bfunc() in line 6. This executes bfunc() and waits for the promise it returns to resolve.
  7. As part of executing bfunc(), you see 2 - b func from line 2 in the console.
  8. bfunc() returns a promise (because it's declared as async) and since there was no await inside of it and no promise returned from it, that promise's state is advanced to fulfilled. This inserts a job in the Promise job queue to call any registered .then() handlers or code using await on this promise at a future time.
  9. Since there was an await in await bfunc() inside of afunc(), the execution of afunc() is suspended until it gets a notification that the promise returned by calling bfunc() has been resolved. That notification does not come yet because it's waiting in the Promise job queue. Meanwhile, afunc() immediately returns a promise in the pending state.
  10. The promise executor returns and the newly created promise is available.
  11. Since this newly created promise is now fulfilled, it inserts a job in the Promise job queue to call any registered .then() handlers or code using await on this promise at a future time.
  12. Then, line 14 executes .then(...) on the newly created promise. This registers a .then() handler for the previously created promise and then returns a new promise.
  13. That new promise is assigned to the variable prom.
  14. Then line 18 executes and you see 3 - last thing as your third line of output.
  15. Then the current chain of Javascript is finished and has nothing more to execute synchronously so it returns control back to the event system.
  16. As one of the first things the event system does when it gets control, the Promise job queue is checked for any pending tasks. The first item in the Promise job queue is the resolved promise from calling bfunc() which was being awaited in line 6. Since the promise was resolved, the execution of afunc() is resumed and you see 4 - a func in the output. afunc() returns which resolved its promise, but nobody is listening to its promise so there's nothing more to do in this chain of Javascript and control goes back to the event system.
  17. Again, it checks the Promise job queue before checking for other things and there's again a pending Promise task. This calls the .then() handler on from the prom promise and you see 5 - foo in the output.

Main Takeaways

  1. await in an async function suspends execution of that function and immediately returns a promise from the function back to the caller.
  2. When promises are resolved or rejected, they always notify completion or error from await, .then(), try/catch or .catch() via the Promise job queue which only services jobs when the current chain of Javascript has returned control back to the event system. So, sequential lines of code keep running until they return control back to the event system. This is why when you see in the above description that a promise is resolved, it inserts a job into the Promise job queue and that job does not run immediately. It is often said that promises always resolve or reject "asynchronously on the next tick of the event loop". While some disagree with the exact meaning of "next tick of the event loop", the concept is correct that promises notify of resolve/reject only AFTER the current thread of Javascript finishes executing and returns control back to the event system. The event system can then look at it's various queues to determine what to do next and one of the first queues it looks in is the Promise job queue where promise resolve and reject events are pending and waiting to notify whoever is listening to them (with await or .then() or .catch().
  3. The afunc() promise chain is completely independent from other promise chains in this code. This is what I refer to as a "fire-and-forget" promise chain because there is no code watching to see if it completes successfully or has an error. This is nearly always a programming error because if it rejects, there's no handler for that rejection and you will get an unhandled rejection error from the system (analagous to an uncaught exception for synchronous code).
  4. Plus, when you have to multiple separate independent promise chains (with real asynchronous operations in them that have indeterminate timing), then it's unpredictable how one promise chain will finish relative to another. This particular code will always run the same way because you don't have real asynchronous operations behind your promises or async functions so all of this code has determinate timing, but that is not the case in real-world code. So, if you need code to run in a specific order, you would connect the two chains and make one depend upon the other so you can control the order.

Your Questions

Are the 2 places commented above discerning events being pushed correct?

Sort of. First off, remember that completed promises notify either await or .then() via the Promise job queue. And, remember that an await inside an async function suspends execution of that function and immediately returns a new promise. So, the first comment on line 6 would more accurately be "afunc() execution suspended, awaiting notification from the promise that bfunc() returns". The second comment on line 14 would be ".then() handler registered and awaiting notification from the promise created on lines 10".

In the case of the await call, is it the async function itself (including its stack environment that is pushed to the event queue?

Not really. Using the same process as generators, async functions can be suspended at a certain point of execution, preserving all their current state. In this case, that function has registered interested in either the fulfilled or reject state chain with the promise that bfunc() returned and when it receives that notification, it will resume execution of the function (if the promise is fulfilled) or will reject its own promise (if the promise is rejected and there's no local handler for that rejection).

So, the function state (which is an object inside the JS interpreter) just sits there waiting to be notified from that promise that bfunc() returned. It did not itself put anything in the event queue. It registered an interest in state changed on the bfunc() promise and then when the bfunc() promise resolves or rejects, then something will be inserted into the Promise job queue to notify registered listeners.

Why does '5 - foo' not print before '4 - a func'?

As I said earlier in takeaways #3 and #4, you have two independent promise chains here because nothing is monitoring the completion of afunc(). The ordering between your last two lines has to do with exactly when the promise state change was added to the Promise job queue for each promise chain. If you cared about this order and these were real asynchronous operations, you would have to program this differently so you don't have two independent and uncoordinated promise chains each with their own unpredictable timing.

You can find the exact sequencing of operations in my description of steps above, but it really isn't all that relevant because you wouldn't use code like this without real asynchronous operations and with real asynchronous operations, the ordering of these two independent promise chains would not be predictable because their timing of when they resolved or rejected their promises relative to one another would not be predictable. So, it's not really worth trying to understand that level of detail as you should never rely on it in your asynchronous programming anyway. If you care about the ordering, then link the two chains so YOUR code determines the ordering, not minute details of timing.

jfriend00
  • 683,504
  • 96
  • 985
  • 979
  • Should be noted that it is quite recent that 4 returns before 5: https://stackoverflow.com/questions/62032674/js-promises-inconsistent-execution-order-between-nodejs-versions/62766147#62766147 In older versions of JS engines, 5 would return first. – Kaiido Jun 07 '21 at 02:32
  • @Kaiido - Noted as a detail related to performance improvements for `await`. But, the bigger point here (as I said several times in my answer) is that one should not be depending upon the relative order of 4 and 5 at all because if these were real asynchronous operations, that order would be indeterminate so would only be controlled with different coding to link the two chains. And, if these were not real asynchronous operations, then we shouldn't be using promises for them. Fortunately, you can safely use promises without understanding or caring about that level of detailed timing. – jfriend00 Jun 07 '21 at 02:36
  • Yes, the fact this behavior did change recently further cement the point that one should not rely on it. – Kaiido Jun 07 '21 at 02:56