I came across this snippet shared by someone when he was trying to understand an async code snippet.
Core Explanation:
The body of an async function can be thought of as being split by zero or more await expressions. Top-level code, up to and including the first await expression (if there is one), is run synchronously. In this way, an async function without an await expression will run synchronously. If there is an await expression inside the function body, however, the async function will always complete asynchronously.
It basically says that the code until first await inside an async function is executed in a sync fashion w.r.t to the outer level code from where the function call to the async function is made.
I tested this as an explanation to the following two code snippets (my slightly altered version of the code that guy who shared the core explanation was trying to demystify):
Original question we aimed to demystify: Why is 'fn one' executing in a sync fashion w.r.t the outer level code or as if we had awaited it, inspite of being declared as async?
Snippet 1:
async function run() {
async function one() {
// meant to simulate as a slightly expensive thing to do
// we can even increase 5000 to a humongous number and the result is still the same
for (let i=0; i<5000; i++) {
console.log('start');
}
}
one();
console.log('end');
}
run ();
Output: 'end' gets printed after all the logs printing 'start'
Snippet 2:
async function run() {
async function one() {
await 1;
for (let i=0; i<5000; i++) {
console.log('start');
}
}
one();
console.log('end');
}
run ();
Output: 'end' gets printed first before all the logs printing 'start'
The explanation at the start seemed to fit well and proved to be right as an explanation to the difference between what is happening in the previous 2 code snippets. I even tried different versions of code snippet 2 to see if things really happen that way and it did look that the core explanation was right.
This implementation however of an async function as a feature in the programming language seemed super weird to me cause for a long time I thought that when we want to perform some async operation or do an expensive thing, basically things that can be done in the background for which we don't want other things to wait, we make the logic or the functional containing the logic as async, essentially meaning that we just trigger the task and now it will be performed/completed asynchronously w/o blocking the code ahead of it but if the implementation of async-await feature is done as described earlier, the language does everything however expensive it may be in a blocking way so to say until we use await even after marking the function as a task to be achieved asynchronously. This seems very un-intuitive as a developer. Look below,
Snippet 3
async function run() {
async function one() {
let a = 1;
// meant to simulate as a slightly computationally expensive thing to do
for (let i=0; i<999999; i++) {
a++;
}
console.log('ran in sync till now');
// code after the function call to this function did not run until this point
await 1;
// rest of the task will be completed in async fashion
for (let i=0; i<5000; i++) {
console.log('start');
}
}
one();
// things to be done without waiting for 'fn one' to complete
console.log('end');
}
run ();
Output: Same as described before, it executed that expensive for loop and then executed and printed 'end' and then went to start printing 'start'.
My #1 question now: does this seem right? Am I missing on something here? Am I the only thing who thinks this is super weird and appears faulty?
For most of the part, everything I described above fitted well, although not to my liking tbh, but I found two versions of code where the core explanation described at the start and proved by code snippets following it, broke and did not make sense.
I wrote this snippet to test my initial theory (prior to coming across the explanation described at the very start). My initial theory about why code snippet #1 worked the way it did was that, due to the async 'function one' doing a possible blocking I/O operation of logging to the console in a long running for loop, therefore even though the execution must have reached the 'end' log, it was not able to print it out cause the I/O is occupied and when its released by the function one, 'end' gets printed out and that why we see 'end' at the last, after all the 'start' logs. So, the execution or JavaScript is not waiting for the 'async fn one' to complete but actually waiting for the I/O and its just appearing as if 'fn one' was called with await.
The anomaly to the explanation: Anomaly Snippet 1
Based on my theory that I/O block is causing things to mess up. I decided to create a separate log stream of my own.
const { Console } = require('node:console');
const fs = require('fs');
const output = fs.createWriteStream('./stdout.log');
const errorOutput = fs.createWriteStream('./stderr.log');
// Custom simple logger
const logger = new Console({ stdout: output, stderr: errorOutput });
async function run() {
async function one() {
for (let i=0; i<100000; i++) {
console.log('start');
}
}
one();
logger.log('end');
}
run ();
Output: Ran this in Node 18. It does print out 'end' to the log file while 'start' is still getting printed.
Anomaly Snippet 2
async function run() {
async function one() {
for(let i=0; i<100000; i++) {
console.log('start');
}
}
one();
alert('end');
}
run ();
Output: Ran this one in browser environment and alert popup came up while 'start' was still printing.
My #2 question: Taking into account the above anomaly snippets, what do I conclude? It did not wait for 'fn one' to complete or in other words, it did not run the the entire thing in a sync fashion even though we haven't put any await inside of 'fn one' as claimed by the core explanation? It just triggered one and came back to execute code present after it? I am shaken and super clouded as to why JavaScript is behaving with such uncertainty. (Although truth be told, I am fully aware that's what JS is famous for but still)
Node version used: 18.15
Chrome version used: 114.0.5735.134
OS details: Windows 11 Home Single Language, Version 22H2, OS build 22621.1848
Also ran things on Ubuntu (18.04)
I tried different things with code snippets shared above but reached different conclusion which didn't converge. I expected a concrete explanation of the way things were working but ended up opening pandoras box.