JS Tip of the Day: Async Functions and await

Async Functions and await
Version: ES2017
Level: Intermediate

Async (i.e. asynchronous) functions are a fairly new kind of function type in JavaScript that allow you to use the await operator. The await operator (which currently is only available in async functions) is used to obtain the result of a promise, pausing the current execution context until the value of that promise is available. Once it is, the await expression resolves to that value and execution continues. Other code around the async function call is free to continue running when this pause happens which is exactly what makes async functions async.

To create an async function, include the async keyword before the function definition. They’re called just like any other function.

async function getFlightStatus () {
    console.log('Starting to get status...');
    let flightStatus = await Promise.resolve('Delayed');
    console.log('Got status!');
    return flightStatus;
}

console.log('Before');
getFlightStatus();
console.log('After');
/* logs:
Before
Starting to get status...
After
Got status!
*/

Here, you can see that once the await in the getFlightStatus() function was reached, the function execution paused and code at its callsite continued to run. This is why the “After” log came before the “Got status!” log.

Because async functions can pause, they will always return a promise. This promise will resolve to the return value of the async function once the async function eventually returns.

let result = getFlightStatus();
console.log(result instanceof Promise); // true
result.then(status => {
    console.log(status); // Delayed
});

Like normal functions, if an async function doesn’t have an explicit return, it will implicitly return undefined. In that case, when called it will return a promise which will resolve to undefined.

async function getFlightRefund () {
    // does not return
}
getFlightRefund().then(refund => {
    console.log(refund); // undefined
});

Using await in async functions is a nice alternative to using the promise API. It lets you write asynchronous promise code much like you would write normal synchronous code. await itself replaces the use of then() while try...catch...finally blocks replace the use of catch() and finally() in promises.

let somePromise = Promise.resolve('I promise');

async function awaitPromise () {
    try {
        let promiseValue = await somePromise;
        console.log(promiseValue); // I promise
    } catch (error) {
        console.error(error.message);
    } finally {
        console.log('Done awaiting promise');
    }
}

awaitPromise();

This is basically the same as

let somePromise = Promise.resolve('I promise');

somePromise.then(promiseValue => {
    console.log(promiseValue); // I promise
}).catch(error => {
    console.error(error.message);
}).finally(() => {
    console.log('Done awaiting promise');
});

While you can currently only use await inside async function, there is a stage 3 proposal for using await in the top level scopes in modules. This would mean in the future, you could await promises without having to wrap them in an async function first (assuming you’re in a module).

More info:

So the basic reason for introducing such functions are mainly an attempt to reduce the code size, nesting and hence the complexity? Just a better way to organise the code? Because they don’t seem to do anything new.
I saw similar changes in coding methodology in Unity too while scripting with C#. It’s called IEnumerator, which is coupled with yield return. And that reduces the code which is otherwise difficult to read if replaced by event-listeners.

It’s true that they don’t necessarily add anything new. Complexity and organization are the real benefits. And with that, consistency in writing code, particularly in being able to write code that looks like synchronous code despite it being asynchronous, all thanks to the help of a couple of keywords (async and await) that helps us escape the promise API completely (most of the time).

Where this especially becomes helpful is allowing a promise chain to occur all in the same scope, especially as you might have dependencies scattered throughout would-be then() callbacks. For example:

getUser()
    .then(user => {
        return user.getAccount();
    })
    .then(account => {
        // but now how do I access user?
    });
    
// vs

let user = await getUser();
let account = await user.getAccount();
// user and account in scope

If you’ve ever worked with async promise code in ES5, you may know the struggles. The lack of arrow functions in combination with the use of the promise API can make for some messy code (yes, I had a bad experience).