The issue seems to be the usage of await
. When the keyword is encountered, the JavaScript interpreter will immediately pause the rest of the function and only resume the rest of the execution after the Promise is resolved. If you await nonPromiseValue
it's treated as if you've created it via Promise.resolve()
and is essentially equivalent to
Promise.resolve(nonPromiseValue)
.then((resolvedValue) => {
resolvedValue;
})
Or even more generally this:
await <await expression>;
<rest of body>;
return <return expression>;
Works like this:
Promise.resolve(<await expression>)
.then(resolvedValue => {
resolvedValue();
<rest of body>;
return <return expression>;
});
Thus and then you'd still have the rest of the body of the function put on the microtask queue and executed at least the next time the event loop picks a task.
NOTE: I'm omitting some details because the engine would be pausing and unpausing the execution, so if the await
expression is part of another statement, you might get slightly different behaviour than what you'd expect at a glance, yet still the reason is immediate pausing when await
is encountered. Thus expressions before and after an await
keyword would be evaluated at different times and thus might have a different result.
At any rate, here is an example:
async function foo() {
console.log("foo - start");
"any value without await";
console.log("foo - end")
}
async function bar() {
console.log("bar - start");
await "any value";
console.log("bar - end")
}
console.log('start');
foo();
bar();
console.log('end');
For foo()
the execution is straight forward:
- Call the function.
- Execute every line in the body.
- Finish.
Yes, it's async
but it's still executed synchronously. The only difference here is that the result would be a Promise but since it's all synchronous and there is no return
keyword, then it's just implicitly Promise.resolve(undefined)
.
However, for bar()
the execution is different:
- Call the function.
- Execute every line until you encounter
await
.
- Turn the rest of the function to a Promise.
- Pause and wait until the Promise is resolved.
- Since we've
await
-ed a non-Promise this will happen immediately the next event loop iteration.
- Continue the execution.
So, in fact the body of the function is wrapped in a Promise behind the scenes, so we actually run something similar to this:
async function bar() {
console.log("bar - start");
Promise.resolve("any value") //<-- await "any value";
.then((resolvedValue) => {
resolvedValue; //<-- nothing is done with it but it's what we awaited
console.log("bar - end"); //<-- the rest of the body of `bar()`
})
}
console.log('start');
bar();
console.log('end');
This is a simplified view but it's to just help visualise the fact that await
will always halt execution and resume later. It uses the same Promise mechanics as usual and would delay the rest of the body for a later iteration of the event loop via a microtask but it's automatically handled for you.
Were we actually await
-ing a real Promise, then you'd still get the equivalent behaviour:
async function bazAwait() {
console.log("bazAwait - start");
const result = await new Promise(resolve => resolve("some Promise value"));
console.log("bazAwait - end", result); //<-- the rest of the body of `bazAwait()`
}
async function bazPromiseEquivalent() {
console.log("bazPromiseEquivalent - start");
new Promise(resolve => resolve("some Promise value"))//<-- the awaited Promise
.then((promiseVal) => { //<-- the value the Promise resolves with
const result = promiseVal; //<-- the binding for the resolved value
console.log("bazPromiseEquivalent - end", result); //<-- the rest of the body of `bazPromiseEquivalent()`
});
}
console.log('start');
bazAwait();
bazPromiseEquivalent();
console.log('end');
I heavily suspect this is done in order to keep the behaviour the same regardless of whether or not await
a Promise. Otherwise if you had a line like await myValue
you'd get different execution depending on what myValue
holds and it will not be obvious until you actually check that.