Async / Await

July 2018

Prerequisits for this article: Familiarity with JavaScript Promises. The article may also contain traces of TypeScript.

Let's dive right in with an example of async / await:

const getDatabaseConnection: () => Promise<DatabaseConnection> = ...
const getUser: (con: DatabaseConnection) => Promise<User> = ...
const getTodos: (con: DatabaseConnection, user: User) => Promise<Todo[]> = ...
const getMyTodos = async () => {
    const con = await getDatabaseConnection();
    console.log('Connection established!');
    const user = await getUser(con);
    console.log('User fetched!');
    const todos = await getTodos(con, user);
    console.log('Todos received.');
    return todos;
}
getMyTodos().then((todos) => {
    console.log(JSON.stringify(todos));
});
    

See a fiddle: https://tsfiddle.tsmean.com/fiddle/YQdJ2dOG

So let's interpret this example. First of all we have three promises, one to get a database connection, one to get the current user and one to get the todos of a user. Then the async/await fun starts. The first important thing we see is that we prefix a function(here it's getMyTodos) with async. Why? Well, the next thing we see is that inside of this async function we don't use the promises how we usually would use promises. Instead of somePromiseInvokation().then(promiseSuccessResult => {...}) we use const promiseSuccessResult = await somePromiseInvokation(). So you might notice that we omitted the error case here, we'll get to that in a minute.

So what we see is that prefixing a function with async gives us a a new ability inside of that function block. Namely to use the await keyword. We also see that the await keyword allows us to consume promises in a more synchronous and simple way than with then / catch. Consider how you would do something similar with promises:

const getMyTodos = () => {
    getDatabaseConnection().then(con => {
        getUser(con).then(user => {
            getTodos(con, user).then(todos => {
                // do some stuff with the todos
            })
        })
    })
}
    

Fiddle: https://tsfiddle.tsmean.com/fiddle/y1aKReQG

The syntax with then / catch is clearly more verbose than the syntax with await. Another very important point to take away from this: async / awit is not a replacement for promises! It's rather a replacement for then / catch, meaning a simpler way of consuming promises. But in order to use async / await, you need some promises in the first place.

The above example of the getMyTodos with then/catch and async/await still aren't fully equivalent. In the async/await case we had a return statement, in the promise case we have 'done the stuff' right inside of the function. Of course this isn't really reusable, what if we'd like to 'do some other stuff' in some other place of our code? So we'd actually need something like this to be equivalent:

const getMyTodos = () => {
    return new Promise(resolve => {
        getDatabaseConnection().then(con => {
            getUser(con).then(user => {
                getTodos(con, user).then(todos => {
                    resolve(todos)
                })
            })
        })
    })
}
getMyTodos.then((todos) => {
    // do some stuff with the todos
});

Fiddle: https://tsfiddle.tsmean.com/fiddle/9wdLwbjP

This reveals the second property of async / await. An async function returns a promise! If you scroll back up to the async / await example, you'll see how on getMyTodos a .then call is made. To be more precise, it's an already resolved promise. But why does async await wrap our return value in a promise, doesn't make this consuming the return value more complicated? Well, in this simple case yes. But consider you have an async function where you want to invoke another async function. Then it's useful that you get a promise, since this allows you to await async functions. All clear? Here's an example, based on the getMyTodos async-function from the top:

async function getMyTodosAndThenUpdateThem() {
  const todos = await getMyTodos();
  // now do the updating...    
}

While we had to create a new Promise manually in the then / catch, the async await automagically returns a promise. So far we have seen, that async await can be much more concise, linear (instead of triangular) and hence readable than the then/catch syntax. But how do we handle errors?

Error handling in async await

Let's first have a look at how we'd handle errors in the then/catch example.

const getMyTodos = () => {
    return new Promise(resolve => {
        getDatabaseConnection().then(con => {
            getUser(con).then(user => {
                getTodos(con, user).then(todos => {
                    resolve(todos)
                }).catch(todoFetchError => {...})
            }).catch(userFetchError => {...})
        }).catch(conError => {...})
    })
}
getMyTodos
    .then(todos => {
        // do some stuff with the todos
    })
    .catch(err => {
        // handle the error.
    });

Fiddle: https://tsfiddle.tsmean.com/fiddle/N1aMAaWm

We could also use one common error handler function handleError that we then pass to all the catches if we don't want to handle each catch individually.

To enhance the example with error handling, we'd extend our async await function with one or more try/catch blocks, leading us to our final async await code:

getDatabaseConnection: () => Promise<DatabaseConnection> = ...
getUser: (con: DatabaseConnection) => Promise<User> = ...
getTodos: (con: DatabaseConnection, user: User) => Promise<Todo[]> = ...
const getMyTodos async () => {
    let todos;
    try {
        const con = await getDatabaseConnection();
        const user = await getUser(con);
        todos = await getTodos(con, user);
    } catch (err) {
        // Handle the error. Either `throw new Error` or if //TODO verify
        // you have a sensible fallback (e.g. getTodosFromOtherDb()) use that.
    }
    return todos;
}
getMyTodos().then((todos) => {
    // do some stuff with the todos
});

Another Example: Sequential For-Loops

Another even more compelling use case is when you have a for-loop, where in each iteration you need to run an asynchrnous operation, but the next iteration needs to wait for the current iteration to finish.

Traditionally, you would do this with recursion:

const updateUser = () => {
    return new Promise((resolve, reject) => {
        // ... do stuff
    })
}
const sequentialUserUpdate = (userList) => {
    [head, ...tail] = userList;
    if (head != null) {
        updateUser(user).then(() => {
            sequentialUserUpdate(tail);
        });
    }
}
sequentialUserUpdate(someUsers);

With async / await the code becomes more readable:

const sequentialUserUpdate = async (userList) => {
    userList.forEach(user => {
        await updateUser(user);
    });
}
sequentialUserUpdate(someUsers);

Summary

Interested in TypeScript?