Search code examples
javascriptasync-awaites6-promise

Javascript async/await behaviour explanation


I always thought that Javascipt's async/await is just sugar syntax, e.g. writing this peace of code

let asyncVar = await AsyncFunc()
console.log(asyncVar)
//rest of code with asyncVar

is equivalent to writing this

AsyncFunc().then(asyncVar => {
   console.log(asyncVar)
   //rest of the code with asyncVar
})

Especially since you can call await on some random, non-promise object and it will try to call it's then() function

However, I tried this peace of code

let asyncFunc = function() {
    return new Promise((resolve,reject) => {
       setTimeout(_ => resolve('This is async'), 1000)
    })
}

for(let i = 0; i < 5; i++) {
   console.log('This is sync')
   asyncFunc().then(val => console.log(val))
}
for(let i = 0; i < 5; i++) {
   console.log('This is sync')
   console.log(await asyncFunc())
}

First loop outputs 'This is sync' 5 times, and then 'This is async' 5 times, as expected. Second loop outputs 'This is sync', 'This is async', 'This is sync', 'This is async' and so on.

Can someone explain what is the difference, or in other words, what exactly async/await does behind the scenes?


Solution

  • Your first two examples are basically equivalent. But, guess what, you can't use a for loop in those two examples. There isn't a simple-to-code way to mimic an await statement inside a for loop. That's because the interaction of a for loop (or a while loop) with async/await is more advanced than plugging in a simple .then() statement as in your two examples.

    await pauses the execution of the entire containing function. This will pause even for and while loops. To program something similarly with only .then(), you have to invent your own looping construct because you can't do it with just a for loop.

    For example, if you have this using await:

    let asyncFunc = function() {
        return new Promise((resolve,reject) => {
           setTimeout(_ => resolve('This is async'), 1000)
        })
    }
    
    async function someFunc() {
        for(let i = 0; i < 5; i++) {
           console.log('This is sync')
           console.log(await asyncFunc())
        }
    }
    

    And, if you want an analogy using only .then(), you can't use the for loop because there's no way to "pause" it without await. Instead, you have to devise your own loop which usually involves calling a local function and maintaining your own counter:

    function someFunc() {
        let i = 0;
    
        function run() {
            if (i++ < 5) {
                console.log('This is sync')
                asyncFunc().then(result => {
                    console.log(result);
                    run();
                });
            }
        }
        run();
    }
    

    Or, some use a .reduce() construct such as this (particularly if you're iterating an array):

    // sequence async calls iterating an array
    function someFunc() {
        let data = [1,2,3,4,5];
        return data.reduce((p, val) => {
            console.log('This is sync')
            return p.then(() => {
               return asyncFunc().then(result => {
                    console.log(result);
                });
            });
        }, Promise.resolve());
    }
    

    Can someone explain what is the difference, or in other words, what exactly async/await does behind the scenes?

    Behind the scenes, the Javascript interpreter suspends further execution of a function at an await statement. That current function context (local variable state, point of execution, etc...) is saved, a promise is returned from the function and execution outside that function continues until sometime later when the promise that was being await ed resolved or rejects. If it resolves, then that same execution state is picked up and the function continues to execute some more until the next await and so on until eventually the function has no more await statements and has no more to execute (or gets to a return statement).

    To explain further, here's some background on an async function:

    When you declare an async function, the interpreter creates a special kind of function that always returns a promise. That promise is rejected if the function either explicitly returns a rejected promise or if there's an exception in the function (the exception is caught by the interpreter and changed into a rejection for the promise that the function returns).

    The promise is resolved if/when the function returns a normal value or just returns normally by finishing its execution.

    When you call the function, it starts out executing synchronously just like any normal function. This includes any loops that might be in the function (as in the for loop in your example). As soon as the function encounters the first await statement and the value it's awaiting is a promise, then the function returns at that point and returns its promise. Further execution of that function is suspended. Since the function returns its promise, any code after that function call continues to run. Sometime later when the promise that was being awaited internal to the async function resolves, an event is inserted into the event loop to resume execution of that function. When the interpreter gets back to the event loop and gets to that event, then execution of the function is resumed where it previous left off on the line right after the await statement.

    If there are any other await statements that are awaiting promises, then they will similarly cause the function execution to be suspended until the promise that is being awaited is resolved or rejected.

    The beauty of this ability to suspend or pause execution of a function in mid-course is that it works in loop constructs such as for and while and that makes executing async operations in sequence using such a loop a ton easier to program that before we had async and await (as you've discovered). There is no simply analagous way to code a sequencing loop using only .then(). Some use .reduce() as shown above. Others use the internal function call as also shown above.