1

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.

Refer to the MDN page

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.

Akash
  • 27
  • 3
  • 4
    Your `one()` function is `async` but you do not `await` it. – Pointy Jun 28 '23 at 18:25
  • 1
    Take a look at https://stackoverflow.com/questions/59129054/does-putting-synchronous-functions-in-an-async-function-still-block-nodejs – James Jun 28 '23 at 18:29
  • 4
    It's not really clear what it is you're trying to test here. The `async` keyword does not *make* a function asynchronous, it simply annotates the fact that it returns a Promise (or a series of Promise instances from internal `await` calls). The fact that your function just burns CPU cycles doesn't really mean anything; it could do nothing at all and the effect would be the same. It's really just a plain function. – Pointy Jun 28 '23 at 18:36
  • @Pointy Not using await is intentional. As I described, sometimes it seems its behaving in a blocking way and sometimes it appears otherwise. Moreover, the larger question is to get an understanding of why is the implementation the way it is? If its a general saying to make computationally expensive things async, then why is having atleast 1 await necessary for an async function to complete asynchronously? – Akash Jun 28 '23 at 18:43
  • @Pointy burning CPU cycles doesn't amount to being computationally expensive? I am a little skeptical on that. – Akash Jun 28 '23 at 18:45
  • 2
    @Akash it *is* expensive, but it's a simple synchronous function. When called, it's going to run until it finishes. So for your test, it's really no different than a function that does nothing. – Pointy Jun 28 '23 at 18:49
  • 3
    And again, **`async` does not make a function asynchronous.**. Being asynchronous due to calls to other asynchronous APIs is what makes a function asynchronous. – Pointy Jun 28 '23 at 18:50
  • @Pointy but if its a simple synchronous function which will run until it finishes, then what about the 2 snippets I marked as an anomaly? Why did alert got executed or log got printed why the simple sync function did not finish its execution? – Akash Jun 28 '23 at 18:54
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/254293/discussion-between-akash-and-pointy). – Akash Jun 28 '23 at 18:54
  • 1
    You're mixing `alert()` and `console.log()`. They do not work the same way. – Pointy Jun 28 '23 at 18:55
  • "*Core Explanation*" - please [do not post a link to a painting of text](https://meta.stackoverflow.com/q/285551/1048572)! Put a quote of the text in your question, cite the author, and link the page where you came across it. – Bergi Jun 28 '23 at 18:55
  • @Bergi I apologize but I can't do that. As I mentioned, it was shared to me as it is. I don't know where it came from. My major concern is with what its saying. – Akash Jun 28 '23 at 18:58
  • @Akash you can still transcribe it to text so that everyone can read it, and you should still mention your source (i.e. *who* shared it). Or with a bit research you might even find the original author and context, i.e. the page where the screenshot was taken. – Bergi Jun 28 '23 at 19:00
  • Your tests have too many unknowns in them - observing console output is not good way of debugging "concurrency". Use some global variable and assign values to it at specific points and then print out that variable. – Erki Aring Jun 28 '23 at 19:01
  • @Pointy Check out the chat. Stackoverflow was suggesting to move the discussion to chat. – Akash Jun 28 '23 at 19:10
  • @ErkiAring Please point out some unknowns explicitly, I am not able to get visibility otherwise as to why such simple pieces of code snippets are behaving in unexpected manners or having unknown as you pointed. Also the intention was not to test, so the code is not written with the aim to test concurrency if I am right. It all started with the code snippet itself and then came the question as why its behaving the way it is. Although console.log was there in the original snippet I was trying to understand, but you can replace it with alert or the custom logger I created as well. – Akash Jun 28 '23 at 19:15
  • @ErkiAring One more thing, you suggested to take a global var, do some manipulations on the way and then finally, print it out. How is that different than printing out some string? At the end we are just printing a value again. Plus you said watching the console output is not a good way. I am a little confused here sorry. – Akash Jun 28 '23 at 19:39
  • @ErkiAring There is no concurrency issue with `console.log`. It always prints the lines in the same order as they were logged - even if buffered (and a little delayed) to optimise rendering of the devtools. The only issues is - as Pointy pointed out before - mixing `console.log` with `alert`, since alert dialogs are not buffered. – Bergi Jun 28 '23 at 20:40
  • Your question is too broad. It contains multiple questions. Please focus on one question only. Yet the main thing seems to be the wrong understanding of asynchrony in JS, mixed with the lazy behaviour of `Console` (devtools). – trincot Jun 29 '23 at 08:07

0 Answers0