async function funcTwo() {
return new Promise((r) => r());
}
async function funcOne() {
console.log("A");
(async () => {
await funcTwo();
console.log("B");
})();
console.log("C");
}
await funcOne();
console.log("Done");
According to my knowledge the output should be as follows:
A
C
B
Done
My reasoning is:
1.funcOne
runs
2.A
is printed
3.async function runs and promise is resolved immediately thus console.log("B")
is moved to the mircotask queue
4.C
is printed
5.funcOne is resolved and console.log("Done");
is moved to the mircotask queue.
6.Tasks are fetched from the queue and B
and Done
are printed in that order. (console.log("B")
is added to the queue before console.log("done")
)
However the output is:
A
C
Done
B
B
and Done
are switched
Can someone explain what I got wrong?
If you have an async
function that executes a return
with a promise object, then the promise that the function returns will be pending, never settled. The promise returned by the async
function will be locked-in to the promise given to the return
statement.
Let's give the involved promises some names. This is essentially the same script, but with variable names for all relevant promises:
async function f2() {
const p0 = Promise.resolve();
return p0;
}
async function f1() {
console.log("A");
async function f3() {
const p2 = f2();
await p2;
console.log("B");
}
const p3 = f3();
console.log("C");
}
const p1 = f1();
const p4 = await p1;
console.log("Done");
Even though p0
is a fulfilled promise, p2
is not. p2
is locked-in to p0
, and internally a then
is attached to p0
to listen for its fulfillment, so that p2
can follow that same state transition, and be fulfilled too. But such (hidden) then
callback needs a promise job to be put in the queue (microtask queue). As a consequence, p0
will not fulfill synchronously, but later.
On the other hand, funcOne
returns a fulfilled promise, as there is no await
it executes itself. Note that the await
in the nested anonymous function only pauses that anonymous function, not funcOne
. The latter will continue to log "B" without considering the state of any promise, and so it returns undefined
as the value of a fulfilled promise.
If you make your reasoning with this principle, you'll see the output you get is the one that is expected.
Here is a simplified view on the sequence of operations. It depicts the callstack at the left, the current action being performed, the state of all relevant promises (F=Fulfilled, P=pending), and the jobs that are in the promise job queue (microtask queue):
Call stack | Action | p0 | p1 | p2 | p3 | Promise job queue |
---|---|---|---|---|---|---|
Script | f1() |
|||||
Script>f1 | log('A') |
|||||
Script>f1 | f3() |
|||||
Script>f1>f3 | f2() |
|||||
Script>f1>f3>f2 | r() |
|||||
Script>f1>f3>f2 | p0 = resolve(...) |
F | ||||
Script>f1>f3>f2 | return p0 |
F | ||||
Script>f1>f3 | p2 = ... |
F | P! | fulfill(p2) |
||
Script>f1>f3 | await p2 |
F | P | fulfill(p2) |
||
Script>f1 | p3 = ... |
F | P | P | fulfill(p2) |
|
Script>f1 | log('C') |
F | P | P | fulfill(p2) |
|
Script>f1 | return |
F | P | P | fulfill(p2) |
|
Script | p1 = ... |
F | F | P | P | fulfill(p2) |
Script | await p1 |
F | F | P | P | fulfill(p2) resume script |
Check queues | F | F | P | P | fulfill(p2) resume script |
|
fullfill(p2) |
fullfill(p2) |
F | F | F | P | resume script resume f3 |
Check queues | F | F | F | P | resume script resume f3 |
|
Script-resumed | log('D') |
F | F | F | P | resume f3 |
Check queues | F | F | F | P | resume f3 |
|
f3-resumed | log('B') |
F | F | F | P | |
f3-resumed | return |
F | F | F | F |