Asynchronous JavaScript: The Complete Guide - Part 5 - Understanding Async/Await

Ronaldo B.August 1, 20228 min read

In this article

In this article, we will talk about async/await. If you wish, you can check the other posts in this series at these links:

  1. Asynchronous JavaScript: The Complete Guide - Part 1 - What is asynchronous code?
  2. Asynchronous JavaScript: The Complete Guide - Part 2 - Callbacks
  3. Asynchronous JavaScript: The Complete Guide - Part 3 - Training Callbacks
  4. Asynchronous JavaScript: The Complete Guide - Part 4 - Using Promises

In the last article, we learned about promises, a very useful feature that JavaScript provides for asynchronous code. When promises were released, we might have thought it was excellent already: they help us create organized, readable, and easy-to-understand and develop code. We might have thought it couldn't get any better. But it did! In June 2017, a new version of EcmaScript was released, version ES2017 or ES8. This version introduced a new concept in the JavaScript language: async functions, or async/await. The goal of these functions is to make using promises even easier. An async function consists of creating a 'normal' function, but one that gains what we could call 'superpowers', becoming a function capable of working with asynchronous code. How is this possible? We can start by analyzing what the words async and await represent.

Async/Await

1 - Async. This word can be used when creating a conventional function. When we add this identifier when creating this function, we define that it will be an asynchronous function, and better yet, it will return a promise! When we use the return expression, we are actually resolving a promise.

2 - Await. This word will be used in order to wait for the resolution of an asynchronous function. If there is a series of asynchronous functions, the await expression will define that the code will only proceed when the previous function is resolved. One very important detail: the await expression will only be accepted in a function that is already asynchronous, that is, that has the async identifier at its beginning.

Let's develop a first example of this concept. We will create a function that will join two strings. This function will be asynchronous. Then, we will call this function using the await identifier. See the script below:

async function joinStrings(string1, string2) {
    return string1 + ' ' + string2;
}
async function init() {
    await joinStrings('Hcode', 'Training').then(result => {
        console.log(result);
    });
}
init();

Explaining: the joinStrings() function will concatenate two strings; it was created with the async identifier, making it clear that it is an asynchronous function. The init() function will call it, but with the await identifier. Additionally, note that we used the .then() method after calling the function, just as we do with a promise. This is possible because the joinStrings() function is indeed a promise, but we do not need to create all of its structure. We only need to add the async word when creating it. Cool, right?

This was a first example, and it was already possible to see that asynchronous functions are very useful. But we might be able to see this even better when we use them to replace a series of promises. Let's see an example of that.

We will create three functions that will manipulate a string: 1) one that will convert the first letter of the string to uppercase; 2) one that will reverse the content of the string, and 3) one that will add text to this string. All of these functions will be promises. See the code to create them:

function firstLetterToUpper(string) {
    return new Promise((resolve, reject) => {
        let firstLetter = string.charAt(0);
        let newString = firstLetter.toUpperCase() + string.slice(1);
        resolve(newString);
    });
}

function reverseString(string) {
    return new Promise((resolve, reject) => {
        let stringReverse = string.split('').reverse().join('');
        resolve(stringReverse);
    });
}

function addHelloToString(string) {
    return new Promise((resolve, reject) => {
        let newString = string + ' - Hello World';
        resolve(newString);
    });
}

Perfect. Our functions have been created and are quite simple to understand. Now let's call them. But let's do it differently: we will call one after another, providing as a parameter for each one the value returned by the previous promise. In addition, we will perform a console.log() for each returned value. The code for requesting these functions is as follows:

firstLetterToUpper('hcode')
    .then(newValue => {
        console.log(newValue);
        reverseString(newValue)
            .then(reverseValue => {
                console.log(reverseValue);
                addHelloToString(reverseValue)
                    .then(helloString => {
                        console.log(helloString);
                    })
            })
    });

When executing this code, we see the following result:

Function result in the console

Very well, it worked without problems. However, note that the call of several promises in sequence became a bit difficult to read and understand. It looks a bit like that callback pyramid we talked about in the previous article. Furthermore, we used the .then() method several times, which makes the code somewhat repetitive. Let's convert this code to asynchronous functions and see the same result using this new concept.

First, the functions will no longer return promises, but only have the async identifier at their beginning. Additionally, we will replace the resolve() method with a return. The code for the functions will look like this:

async function firstLetterToUpper(string) {
    let firstLetter = string.charAt(0);
    let newString = firstLetter.toUpperCase() + string.slice(1);
    return newString;
}

async function reverseString(string) {
    let stringReverse = string.split('').reverse().join('');
    return stringReverse;
}

async function addHelloToString(string) {
    let newString = string + ' - Hello World';
    return newString;
}

The code is already much cleaner. Now let's call each function and execute console.log(), but using await. The code will look like this:

let upperValue = await firstLetterToUpper('hcode');
console.log(upperValue);

let reverseValue = await reverseString(upperValue);
console.log(reverseValue);

let helloValue = await addHelloToString(reverseValue);
console.log(helloValue);

Note that we added the returned value of each function into a variable, another interesting resource we can use when working with async/await. Additionally, since one function is called below the other, independently, it seems that our code is synchronous, more intuitive, easy to understand, but 'under the hood' asynchronous code is occurring.

Before executing this code, we just need to do one more thing: remember we said that await only works in an asynchronous function? So, to avoid errors, let's add the function calls within a main function, which we can call callFunctions(). The final code will look like this:

async function callFunctions() {

    let upperValue = await firstLetterToUpper('hcode');
    console.log(upperValue);

    let reverseValue = await reverseString(upperValue);
    console.log(reverseValue);

    let helloValue = await addHelloToString(reverseValue);
    console.log(helloValue);

}
callFunctions();

If we execute this code, we will get the same result:

Function result in the console

Excellent! It worked in the same way, but our code became more streamlined, organized, and easy to understand. These are the advantages of using asynchronous functions.

Handling Errors

We know that promises catch errors with the .catch() method. But what about asynchronous functions, how do they handle it? We have this same method available, because asynchronous functions return promises, so we can access the same resources.

Instead of using the reject() method in an asynchronous function, we can simply use the throw new Error() code. Then, in the function's response, use the .catch() method or use a try / catch block. We thus have two options to use.

Now that we understand the concept of asynchronous functions, let's implement them in our Hcode Games project.

Practicing

We will first alter the requests.js file. Currently, the functions in this file are returning promises and working with the Fetch API, which also returns a promise. Let's adapt both to async/await. The getGameByName() function will look like this:

async function getGameByName(name) {
    try {

        let result = await fetch(`https://api.rawg.io/api/games?search=${name}`);

        result = await result.json();

        return result;

    } catch (error) {
        console.error(err);
    }
}

Let's understand the changes together. We removed the declaration of return new Promise() and are using a try/catch block to perform the operations in the function. Additionally, note that we used await in the call to the fetch() function because it returns a promise. Additionally, we used await in the line below it as well, since the .json() method is also a promise. Finally, note that the code became much simpler with these changes. Let's do the same for the getRelatedGamesByName() function. Here is its code:

async function getRelatedGamesByName(name) {
    try {

        let result = await fetch(`https://api.rawg.io/api/games/${name}/suggested`);

        result = await result.json();

        return result;

    } catch (err) {
        console.error(err);
    }
}

Great. We can move on to the HTML file. Now, when calling the functions, we will no longer use the .then() method. Instead, we will store the value returned by the function in a variable and continue the code below it normally, as if it were synchronous code. For that, we will need to change the initGames() function to an async function. Its code will look like this:

async function initGames(gamename) {
    setGameLoad(gamesListPrincipal);
    gamesListRelationed.innerHTML = '';
    let games = await getGameByName(gamename);
    gamesListPrincipal.innerHTML = '';
    games.results.forEach(game => {
        let divGame = setGameHTML(game);
        divGame.addEventListener('click', async e => {
            setGameLoad(gamesListRelationed);
            let gameTag = e.currentTarget;
            let gamename = gameTag.dataset.gamename;
            let gamesRelationed = await getRelatedGamesByName(gamename);
            gamesListRelationed.innerHTML = '';
            gamesRelationed.results.forEach(game => {

                let divGameRelationed = setGameHTML(game);

                gamesListRelationed.append(divGameRelationed);

            });

        });

        gamesListPrincipal.append(divGame);

    });

}

Our function is now asynchronous and is waiting for the returned value from the promises. Just one important detail: note that it was necessary to add the async identifier in the addEventListener() method, as this method creates a new function, and inside it, we use await.

After these changes, let's test our project (we saved the best game for last):

Hcode Games game list

And our project is working without problems! Very good, we managed to adapt our site to async/await and now we have a project that was shaped by various concepts of asynchronous code: callbacks, promises, and finally asynchronous functions. You can access all the codes developed in this series at this link: https://github.com/hcodebr/javascript-assincrono-blog.

We have reached the end of our journey through asynchronous JavaScript. With the knowledge we have gained together in these articles, we are now prepared to decide when we will use each of these resources in the situations we might encounter during our development. We hope you enjoyed this series and that it helps you in your learning of JavaScript.

Is there any other topic you would like to see here on our Blog? You can send your suggestions by clicking here.

Thank you very much for reading this article! See you in the next one :)

Keep learning with Hcode

Sign up for the course platform and follow the next blog guides.