Categories


Recent Posts


Archives


What is Async/Await Good For?

astorm

Frustrated by Magento? Then you’ll love Commerce Bug, the must have debugging extension for anyone using Magento. Whether you’re just starting out or you’re a seasoned pro, Commerce Bug will save you and your team hours everyday. Grab a copy and start working with Magento instead of against it.

Updated for Magento 2! No Frills Magento Layout is the only Magento front end book you'll ever need. Get your copy today!

The unstated point of this series was to get us to a place where we could understand what the heck is going on with async/await in modern javascript. We’re finally here!

When I first read the description of async/await I was a little confused. I didn’t get it. It wasn’t until I understood async/await’s provenance that its value showed itself. Now that we’ve taken that same journey together, we’re ready to discuss the latest and greatest in asynchronous programming thought technology from the javascript world.

What is an Async Function

An “async” function in javascript is a regular-old-function with a few super powers. To define an async function, prefix its definition with the async keyword.

// File: hello-async.js

// const notAnAsyncFunction = function() {
//    console.log("Hello Not Async")
// }

const helloAsync = async function() {
    console.log("Hello Async")
}

// or, if you're into javascript's new arrow function syntax
// const helloAsync = async () => {
//    console.log("Hello Async")
//}

helloAsync()

Run the above program, and you’ll get the ever ubiquitous Hello X output.

$ node hello-async.js
Hello Async

Before we get to the super powers, we should talk about what an async function isn’t. An async function will not, by default, run your code asynchronously. Consider the following modified program.

// File: hello-async.js

const helloAsync = async () => {
    /*2.*/ console.log("Hello Async")
}

/*1.*/ console.log("Before Function Call")
helloAsync()
/*3.*/ console.log("After Function Call")

If we run this program, we’ll get the following output.

$ node hello-async.js
Before Function Call
Hello Async
After Function Call

We can see that program outputs its console.log calls sequentially — the work in helloAsync is NOT done asynchronously.

You also may have noticed the numbered comments (/*1.*/) throughout the program. Keep an eye out for these sort of comments throughout this article — they’re there to help you understand the order your code executes in as we start exploring the asynchronous use cases of async functions.

Promise me Always

So, we know what async functions aren’t — but what ARE they? The first super power of an async function is it always returns a promise. Give the following code a try

// File: hello-async.js

const helloWorld = async function() {
    return "Hello World"
}
console.log(helloWorld())

We might expect this small program to output the string Hello World. However, if we run it

$ node hello-async.js
Promise { 'Hello World' }

we see that it returns an instantiated Promise object, and that this object is already fulfilled/settled to the value we returned (the string Hello World). The async function took our returned string, and turned it into a promise. (if you’re not sure what it means for a promise to be fulfilled/settled, we’ve got you covered)

If we wanted to get at this promise’s value in our program, we’d need to do the standard thing with its then method

// File: hello-async.js

const helloWorld = async function() {
    return "Hello World"
}
let promise = helloWorld()
promise.then(function(result){
    console.log(result)
})

If you return an explicit promise from an async function it gets — a little more complicated. More complicated than we want to get into for this article. The short version is the async function returns a new promise that will eventually resolve to your returned promise’s resolved value. If you run this code

// File: hello-async.js

const helloWorld = async function() {
    const promise = new Promise(function(resolve) {
        setTimeout(function(){
            resolve("Hello Promise")
        })
    })
    return promise
}
const promise = helloWorld()
console.log(promise)
promise.then(function(result) {
    console.log("] " + result)
})

you’ll get back the a promise that settles to the value Hello Promise.

$ node hello-async.js
Promise { <pending> }
] Hello Promise

Interestingly, if you return any object with a then method,

// File: hello-async.js

const helloWorld = async function() {
    const promise = {
        then: function() {
        }
    }
    return promise
}
const pendingPromise = helloWorld()
console.log(pendingPromise)

an async function will treat it like a promise.

$ node hello-async.js
Promise { <pending> }

These objects are sometimes called “then-able”. Because of this behavior, you can use any third party promise library you’d like with async functions, (so long as that library uses the then method).

Await

OK — async functions always return promises. That seems interesting, but not all that useful? To understand why an async function always returns a promise, we need to understand the second super power of async functions: The ability to fulfill a promise using the await statement.

When you’re writing code inside an async function, you can use the await statement. This statement is not available outside of an async function.

$ node
> const promise = new Promise(function(){})
undefined
> await promise
await promise
^^^^^

SyntaxError: await is only valid in async function

The await statement expects to have a promise object on its right side. When you use the await statement, javascript will pause the execution of your async function, wait for the promise to return a value, and then continue executing.

That is, consider the following program

// File: hello-async.js
const createPromise = function(message) {
    const promise = new Promise(function(resolve, reject){
        setTimeout(function(){
            if(message) {
                /*1.*/ resolve(message)
            } else {
                reject("No Message Provided")
            }
        }, 0)
    })
    return promise
}

const promise = createPromise("Hello Promise")

const main = function(promise) {
    /*2.*/ console.log("Starting Main")
    promise.then(function(result){
        /*4.*/ console.log(result)
    })
    /*3.*/ console.log("Ending Main")
}
main(promise)

This is another of our small, silly programs we’ve been using to demonstrate how promises work. This program creates a promise object that will return the text Hello Promise asynchronously. Then, in the function named main, we ask that promise for its value and then log that value. When we execute the program, we get the following output.

$ node hello-async.js
Starting Main
Ending Main
Hello Promise

That is — before the asynchronous work in the promise runs, the main function completes execution. Promises, by themselves, force us to use a callback to get the value of the promise. We have no way to get the promise’s value in main’s scope.

Now, consider the same program, but using an async function and the await statement.

// File: hello-async.js
const createPromise = function(message) {
    const promise = new Promise(function(resolve, reject){
        setTimeout(function(){
            if(message) {
                /*2.*/ resolve(message)
            } else {
                reject("No Message Provided")
            }
        }, 0)
    })
    return promise
}

const promise = createPromise("Hello Promise")

const main = async function(promise) {
    /*1.*/ console.log("Starting Main")
    const message = await promise
    /*3.*/ console.log(message)
    /*4.*/ console.log("Ending Main")
}
main(promise)

If we run this program, we get the following output.

$ node hello-async.js
Starting Main
Hello Promise
Ending Main

That is — our main function will log the first line

// File: hello-async.js

console.log("Starting Main")

Then, when we use await, the function will stop and wait for the promise to execute, and then we log its results

// File: hello-async.js

const message = await promise
console.log(message)

and then finally, we log the last message.

// File: hello-async.js
console.log("Ending Main")

In other words, even though we’re using promises, the code in this function executed in a way that we can reason about synchronously.

This is the power of async/await. The original continuation passing style async APIs created “callback-heck”, with many layers of nested callbacks for each async context. Promises cleaned this up a bit, and allowed us to confine things to a single level of callbacks. The async/await combination is the next evolution of this — with await, we can get the results of our asynchronous work without needing to use a callback.

The Opposite of Async?

The async/await combination is a powerful one — but there may be something nagging you in the back of your head.

Then, when we use await, the function will stop and wait for the promise to execute, and then we log its results

If await waits for execution to finish — how is this asynchronous code? That sounds like the opposite of asynchronous code.

Based on everything we’ve already told you — your concern is valid. However, we still haven’t talked about how async functions interact with the rest of your program.

While your async function is waiting for the promise to resolve, javascript will jump back to the calling context and continue executing the main program. Consider the results if we replace our main function and its invocation with the following

// File: hello-async.js

/* ... */

const main = async function(promise) {
    console.log("Starting Main")
    const message = await promise
    console.log(message)
    console.log("Ending Main")
}
console.log("Before Main")
main(promise)
console.log("After Main")

If we run this program, we get the following output.

$ node
Before Main
Starting Main
After Main
Hello Promise
Ending Main

That is, our program executes in the following order

const main = async function(promise) {
    /* 3.*/ console.log("Starting Main")
    /* 4.*/ const message = await promise
    /* 6.*/ console.log(message)
    /* 7.*/ console.log("Ending Main")
}
/* 1.*/ console.log("Before Main")
/* 2.*/ main(promise)
/* 5.*/ console.log("After Main")

While code inside main executes in order, calling main means our entire program does not execute in order. Instead execution leaves the main function, finishes executing in the main scope, and then returns to finish executing main.

Returning Twice?

This execution model presents a few ambiguities. For example, consider an async function that looks like this

// File: hello-async.js

const createPromise = function(message) {
    const promise = new Promise(function(resolve, reject){
        setTimeout(function(){
            if(message) {
                /*1.*/ resolve(message)
            } else {
                reject("No Message Provided")
            }
        }, 0)
    })
    return promise
}

const someFunction = async function() {
    const promise = createPromise("Hello Async")
    const toReturn = await promise

    return "The Promise Returned: " + toReturn
}

result = someFunction()
console.log(result)

If await causes the function to return early — what’s in result? It can’t be "The Promise Returned: " + toReturn, because that code hasn’t run yet. Let’s run the program and find out.

$ node hello-async.js
Promise { <pending> }

This is why an async function always returns a promise. There’s no way our program can know the actual return value of the function when we use await. Instead, the function call returns a promise. If you need the value of something returned by an async function? You either use the promise’s then method or, if you’re in another async function, await the returned promise.

Wrap Up

It took me a while to understand how async/await was an overall improvement to the state of async affairs. While async/await does simplify reasoning about the execution of individual functions that use promises, you still have to deal with your programs becoming increasingly non-linear. This didn’t seem like a clear win to me.

What I realized is async/await offers a clear, crisp separation between the systems programmer and the client programmer. Client programmers are able to use a promise as though it was a synchronous bit of work, but behind the scenes it’s not really blocking. The systems programmer takes on the burden of managing the asynchronous complexity of the overall program.

If you think about how many systems work these days, this makes sense. In a routing framework like express, the average client programmer doesn’t really think about what the express internals are doing — they just write their routing function. In a routing framework that supports async/await, the client programmer can write their routes and await ... till their hearts content. It’s the express core team who will need to deal with the promises returned by these routing methods/functions.

The async/await pattern gives service developers the opportunity to remove the complexity of asynchronous programming from client code. While it’s still early days, as more frameworks start building their systems with await in mind, modern javascript will be slightly more accessible to programmers who don’t already have deep context on writing asynchronous code.

Copyright © Alan Storm 1975 – 2019 All Rights Reserved

Originally Posted: 5th August 2019