Async/Await in JavaScript Explained

Subscribe to my newsletter and never miss my upcoming articles

Introduction

I have previously written articles on Callback & Promises which I recommend you read first to understand the absolute fundamentals of asynchronous processing

With the introduction of Async/Await in ES7, the code looks and behaves more like a synchronous code. However, you should know that async/await is basically syntax sugar built on top of promises.

How does Async/Await work?

Async - The keyword 'async', when added before a function, indicates that it returns a promise & the functions inside it are asynchronous in nature and are denoted by the keyword 'await'.

Await - The keyword 'await' can only be used within a function that is defined with the 'async' keyword. The 'await' tells the JavaScript engine to ensure that the execution is paused until the function completes execution and returns a promise.

Let us look at the below code snippet to understand it better.

Without Async/Await:

Let us assume we have a function getCake, that returns the cake. There are two more functions, buyEggs and bakeCake. In order to bakeCake, we need to buyEggs first. However, in the buyEggs function, there's a timeout set to 2 seconds , which means, the bakeCake function will run immediately and buyEggs function will run after the time interval of 2 seconds.

Hence, the output 'undefined' (since the variable 'eggs' isn't assigned a value yet) & 'Cake' is displayed as the output on the console.

//Function getCake calls the buyEggs & bakeCake functions
//the code execution will not wait for Promise to be resolved
const getCake = function() {
    //Buy Eggs
    const eggs = buyEggs();
    console.log(eggs); //Output -> undefined

    //Bake Cake
    const cake = bakeCake();
    console.log(cake); //Output -> Cake on the console
}

//Function Buy Eggs returns a promise after 2 seconds
const buyEggs = function() {
    setTimeout(() => {
        return 'Eggs';
    }, 2000);    
}

//Bake cake returns cake - But Cake can only be baked after buying eggs
const bakeCake = function() {
    return 'Cake';
}

//Call the getCake() async method
getCake();

//Program Output
//undefined
//Cake

After adding Async/Await:

In order to ensure that buyEggs function runs before bakeCake function, first, you'll need to return a promise from buyEggs function.

The next step would be to add 'async' keyword to the getCake function to indicate that there are asynchronous functions inside the function.

Further, add the keyword 'await' before the buyEggs function to indicate to the JavaScript engine, that the code execution should be paused until the promise is resolved from the buyEggs function.

//Function getCake calls the buyEggs & bakeCake functions
//The async keyword to the getCake function indicates that the function needs to be run asynchronously
//The await keyword to function call buyEggs ensures that 
//the code execution will not proceed unless the promise is returned from buyEggs()
const getCake = async function() {
    //Buy Eggs
    const eggs = await buyEggs(); //Adding await ensures that code execution is paused until promise is resolved
    console.log(eggs); // Output -> Eggs

    //Bake Cake
    const cake = bakeCake();
    console.log(cake); // Output -> Cake
}

//Function Buy Eggs returns a promise after 2 seconds
const buyEggs = function() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve('Eggs');
        }, 2000);
    });
}

//Bake cake returns cake - But Cake can only be baked after buying eggs
const bakeCake = function() {
    return 'Cake';
}

//Call the getCake() async method
getCake();

// Program Output
//Eggs
//Cake

Async returns Promise by default

In the above example, we wrapped the buyEggs function to return a promise. But, by adding an 'async' keyword before any function, it returns a promise implicitly.

The first code snippet below contains the keyword 'async' added before the buyEggs function. In the second example, the function buyEggs function explicitly returns a promise.

What I wanted to show in the example was how the function behaves internally, when the keyword 'async' is added in front of it.

//The below function will return a promise when the 'async' keyword is added 
async function buyEggs(){
    //Do something
}

//The above is same as the below one
function buyEggs() {
    const promise = new Promise((resolve, reject) {
        //Do something
    });
    return promise; 
}

Let us look at an actual code example

//The Sum function is indicated with the 'async' keyword
//Hence the sum of two numbers x & y is wrapped inside a promise
async function sum(x, y) {
    return x + y;
}

//When the async function 'sum' is invoked
//It returns a promise and the return value can be accessed using 'then'
sum(2, 3).then(result => console.log(result));

As you can see from the above code example using async before a function will implicitly have a promise returned. Since a promise is returned, the return value can be accessed using the 'then' keyword.

What happens when you use Await without Async?

Let us take the above getCake example and see what happens when we remove the async keyword but retain the await keyword beside the buyEggs function.

/*
    getCake Function without the async keyword
    await is added to the buyEggs function
*/
const getCake = function() {
    //Buy Eggs
    const eggs = await buyEggs(); //Adding await ensures that code execution is paused until promise is resolved
    console.log(eggs); // Output -> Eggs

    //Bake Cake
    const cake = bakeCake();
    console.log(cake); // Output -> Cake
}

//Output -> Uncaught SyntaxError: await is only valid in async function

As you can see, a syntax error is thrown saying 'await' can only be used inside an async function. I think the reason for this is because when the JavaScript notices the keyword 'await', it first looks up the parent 'async' function it is present within and when it cannot locate one, it ends up complaining that you've violated the declarative rules of async/await.

Error Handling

Finally, the last topic about async/await is how we'll need to approach error handling. If you remember from the promises example, we had a 'then' as well as a 'catch' block which was used for error handling.

Using Promise - catch block for error handling

//Using Promises
const someAsyncFn = function() {
    return new Promise((resolve, reject)) {
            if(someCondition) {
                    resolve(data);
            } else {
                    reject(err);
            }
    }
}

//Invoking someAsyncFn
someAsyncFn
.then(data => console.log(data));
.catch(err => console.log(err)); //Error Handling is done through the 'catch' block

Using Async/Await - Error Handling using try/catch block

In the below example, the code inside the 'async' function fetchFruits is wrapped within a try and a catch block. When the promise returns 'Resolved' then the 'updateUI' function is invoked.

When Promise is Resolved:

//Using Async Await
const fetchFruits = async function() {
    try {
        const fruits = await getFruits();
        updateUI(fruits);
    } catch (e) {
        showError(e);
    }
}

function getFruits() {
    return new Promise((resolve, reject) => {
        resolve(['apple', 'orange', 'banana']);
    });
}

function updateUI(items) {
    let output = '';
    items.forEach(item => {
        output += `
        <li>${item}</li>        
        `
    })
    const list = document.querySelector('.list-item');
    list.innerHTML += output;
}

function showError(e) {
    const error = document.querySelector('#error');
    error.appendChild(document.createTextNode(e));
}

fetchFruits();

When the promise is rejected, the 'showError' function defined within the catch block will be executed as shown in the code below.

When Promise is rejected:

//Using Async Await
const fetchFruits = async function() {
    try {
        const fruits = await getFruits();
        updateUI(fruits);
    } catch (e) {
        showError(e);
    }
}

function getFruits() {
    return new Promise((resolve, reject) => {
        reject(['apple', 'orange', 'banana']);
    });
}

function updateUI(items) {
    let output = '';
    items.forEach(item => {
        output += `
        <li>${item}</li>        
        `
    })
    const list = document.querySelector('.list-item');
    list.innerHTML += output;
}

function showError(e) {
    const error = document.querySelector('#error');
    error.appendChild(document.createTextNode(e));
}

fetchFruits();

You can play around with the code over here

The biggest benefit of using async/await is that it makes code much more readable and maintainable. It makes the code feel it's streamlined and structured similarly as though it is synchronous.

Conclusion

A quick recap of what we've covered in this article:

  • What is Async/Await?
  • How do they work?
  • Async functions return promise by default.
  • Error Handling

I hope you enjoyed the three-part series on asynchronous functions. Don't forget to subscribe to my newsletter & connect with me on Twitter @skaytech

You will also enjoy the following articles:

Comments (1)

Madza's photo

The modern syntax is awesome 🔥🔥