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
- Line 10 executes. This causes
const promise = new Promise(...).then(...)
to begin to execute
- 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.
- As the promise executor is called synchronously, you see the first line of output
1 - inside promise
from executing line 11.
- Then line 12 executes and the promise's state is changed to fulfilled with the value
'5 - foo'
.
- Then line 13 executes and calls
afunc()
.
- 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.
- As part of executing
bfunc()
, you see 2 - b func
from line 2 in the console.
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.
- 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.
- The promise executor returns and the newly created promise is available.
- 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.
- 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.
- That new promise is assigned to the variable
prom
.
- Then line 18 executes and you see
3 - last thing
as your third line of output.
- Then the current chain of Javascript is finished and has nothing more to execute synchronously so it returns control back to the event system.
- 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.
- 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
await
in an async
function suspends execution of that function and immediately returns a promise from the function back to the caller.
- 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()
.
- 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).
- 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.