September 22, 2020

Thoughts on Async and Await in javascript

Introduction:

Two of the more recent additions to the JavaScript programming language are the await and async keywords. When used together these two keywords allow the developer to work with asynchronous code as though it were synchronous. To be entirely clear, this feature is syntactic sugar. It allows the developer to write cleaner code – which should always be a goal of ours.

‘async’ can be used when declaring a function to make the function return a promise – meaning it is asynchronous. The result it returns is a thing that, at some point in the future, promises to be something – just not at the time the function is executed and returned. 

A promise promises to be something at some point in the future. A good example of this would be an AJAX call, when making the call nothing is immediately returned, but at some point in the future the server will respond and the promise will be resolved with some value – this could be some JSON or HTML.

Please subscribe for web development tutorials

Moving onto the code. Let’s take a look at this rather boring function:

const sayHello = () => {
    return "hello world"
}
 
console.log(sayHello());

As you can see, there’s not a lot going on here. We are calling the sayHello function and printing the result. Running this using node or the browser we see the expected result printed to the screen / console:

hello world

Not exactly groundbreaking, but a good starting point for all coders. Now let’s add the async keyword and take a look at the output of the function:

Using Async keyword

const sayHello = async () => {
    return "hello world"
}
 
console.log(sayHello());

What the browser or node now tells us when running this code is that what we get back is a promise. Marking a function as asynchronous in this manner grants us access to use the other keyword in the title of this article.

Using await keyword

The await keyword allows us to call a function that is asynchronous as though it were any other regular function. For example, consider that sayHello makes a call to a function named getMessage to retrieve what to display to the user. This function will return a Promise to emulate what would be returned when using something like fetch to retrieve data from a server:

const getMessage = () => {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve("hello from getMessage");
        })
    })
}
 
const sayHello = async () => {
    return getMessage().then((result) => {
        return Promise.resolve(result);
    })
}
 
sayHello().then((result) => {
    console.log(result);
})

Ignore what is inside getMessage, it’s not really important. Just know that it acts as though an AJAX call would. What we really want to focus on here is the sayHello function and how it calls getMessage().then(…). This bit of code seems to contain a lot of boilerplate and can certainly be improved

Because we have marked sayHello as an async function we can tidy up the code in this function:

const sayHello = async () => {
    return await getMessage();
}

How much easier does that read?

When we run this code we can see that the output is the same as before. Using the await keyword extracts the value that is returned when the promise returned by getMessage() is resolved. In other words, sayHello returns “hello from getMessage”.

But what happens if an exception occurs in getMessage?

Handling exceptions

Let’s first take a look as some code that isn’t using await:

const getMessage = () => {
    return new Promise((resolve, reject) => {
        throw("Something broke");
    });
}
 
const sayHello = async () => {
    return getMessage().then(res => {
        return Promise.resolve(res);
    }).catch((e) => {
        return Promise.reject('We have caught an exception: ' + e);
    })
}
 
sayHello().then((result) => {
    console.log(result);
}, function(result) {
    console.log(result);
});

sayHello is calling the getMessage function, which returns a promise. We are then handling two situations, when the promise returned in getMessage is resolved or rejected. As we are throwing an exception the promise is considered to be rejected. This exception could occur naturally in the code you write, for example should you try to access something that does not exist. Equally, you might manually call Promise.reject(…) in a situation where this makes sense. For example if this function was making an AJAX call to the server, if no message was returned you might choose to manually reject the promise – irrespective of whether the server responded with an error code.

const getMessage = () => {
    return new Promise((resolve, reject) => {
        throw("Something broke");
    });
}
 
const sayHello = async () => {
    try {
        var message = await getMessage();
        return Promise.resolve(message);
    } catch(e) {
        return Promise.reject("We have caught an exception: " + e);
    }
}
 
sayHello().then((result) => {
    console.log(result);
}, function(result) {
    console.log(result);
});

This is easier to read, particularly if you are used to working with more traditional JavaScript – or indeed other languages. Using the async keyword means we can use this method as if we were writing procedural, synchronous code.

Solving deep nesting problem

One of the main problems async / await solves is deep nesting. Consider this example:

const getMessage = async () => {
    ...
}
 
const processMessage = async (message) => {
    ...
}
 
const testMessage = async (message) => {
    ...
}
 
const sayHello = async () => {
    return getMessage().then(message => {
        return processMessage(message).then(message => {
            return testMessage(message).then(message => {
                return Promise.resolve(message);
            })
        })
    })
}
 
sayHello().then((result) => {
    console.log(result);
});

Let’s focus on the sayHello function here. You can see how nesting multiple promises can start to get confusing. Using async we can rewrite the sayHello function as:

const sayHello = async () => {
    var message = await getMessage();
    message = await processMessage(message);
    message = await testMessage(message);
 
    return message;
}

I’m sure you will agree this is much easier to read and understand. With nested promises, the more levels there are, the more difficult it is to understand the code.

ECMAScript 2017 includes many changes, most of which are syntactic sugar. Async / await are just one example. It all helps you to write cleaner, more maintainable code.

Leave a Reply

Your email address will not be published. Required fields are marked *