Asynchronous programming with async / await

Good day, friends!

Relatively new JavaScript additions are asynchronous functions and the await keyword. These features are basically syntactic sugar over promises (promises), making it easy to write and read asynchronous code. They make asynchronous code look like synchronous. This article will help you figure out what's what.

Conditions: basic computer literacy, knowledge of the basics of JS, understanding the basics of asynchronous code and promises.
Purpose: To understand how promises are made and how they are used.

Async / await basics


Using async / await has two parts.

Async keyword


First of all, we have the async keyword, which we put before the function declaration to make it asynchronous. An asynchronous function is a function that anticipates the ability to use the await keyword to run asynchronous code.

Try typing the following in the browser console:

function hello(){ return 'Hello' }
hello()

The function will return 'Hello'. Nothing unusual, right?

But what if we turn it into an asynchronous function? Try the following:

async function hello(){ return 'Hello' }
hello()

Now a function call returns a promise. This is one of the features of asynchronous functions - they return values ​​that are guaranteed to be converted to promises.

You can also create asynchronous functional expressions, like so:

let hello = async function(){ return hello() }
hello()

You can also use arrow functions:

let hello = async () => { return 'Hello' }

All these functions do the same thing.

In order to get the value of a completed promise, we can use the .then () block:

hello().then((value) => console.log(value))

... or even so:

hello().then(console.log)

Thus, adding the async keyword causes the function to return a promise instead of a value. In addition, it allows synchronous functions to avoid any overhead associated with running and supporting the use of await. A simple addition of async in front of the function enables automatic code optimization by the JS engine. Cool!

Keyword await


The benefits of asynchronous functions become even more apparent when you combine them with the await keyword. It can be added before any promise-based function to make it wait for the promise to complete, and then return the result. After that, the next block of code is executed.

You can use await when calling any function that returns a promise, including Web API functions.

Here is a trivial example:

async function hello(){
    return greeting = await Promise.resolve('Hello')
}

hello().then(alert)

Of course, the above code is useless, it only serves as a demonstration of the syntax. Let's move on and look at a real example.

Rewriting code on promises using async / await


Take the fetch example from the previous article:

fetch('coffee.jpg')
.then(response => response.blob())
.then(myBlob => {
    let objectURL = URL.createObjectURL(myBlob)
    let image = document.createElement('img')
    image.src = objectURL
    document.body.appendChild(image)
})
.catch(e => {
    console.log('There has been a problem with your fetch operation: ' + e.message)
})

You should already have an understanding of what promises are and how they work, but let's rewrite this code using async / await to see how simple it is:

async function myFetch(){
    let response = await fetch('coffee.jpg')
    let myBlob = await response.blob()

    let objectURL = URL.createObjectURL(myBlob)
    let image = document.createElement('img')
    image.src = objectURL
    document.body.appendChild(image)
}

myFetch().catch(e => {
    console.log('There has been a problem with your fetch operation: ' + e.message)
})

This makes the code much simpler and easier to understand - no .then () blocks!

Using the async keyword turns a function into a promise, so we can use a mixed approach from promises and await, highlighting the second part of the function in a separate block in order to increase flexibility:

async function myFetch(){
    let response = await fetch('coffee.jpg')
    return await response.blob()
}

myFetch().then((blob) => {
    let objectURL = URL.createObjectURL(blob)
    let image = document.createElement('image')
    image.src = objectURL
    document.body.appendChild(image)
}).catch(e => console.log(e))

You can rewrite the example or run our live demo (see also the source code ).

But how does it work?


We wrapped the code inside the function and added the async keyword before the function keyword. You need to create an asynchronous function to determine the code block in which the asynchronous code will run; await only works inside asynchronous functions.

Once again: await only works in asynchronous functions.

Inside the myFetch () function, the code is very much like the version on promises, but with some differences. Instead of using the .then () block after each promise-based method, just add the await keyword before calling the method and assign a value to the variable. The await keyword causes the JS engine to pause code execution on a given line, allowing another code to execute until the asynchronous function returns a result. Once it is executed, the code will continue execution from the next line.
For instance:

let response = await fetch('coffee.jpg')

The value returned by the fetch () promise is assigned to the response variable when the given value becomes available, and the parser stops at that line until the promise is completed. As soon as the value becomes available, the parser moves on to the next line of code that creates the Blob. This line also calls the promise-based asynchronous method, so here we also use await. When the result of the operation returns, we return it from the myFetch () function.

This means that when we call the myFetch () function, it returns a promise, so we can add .then () to it, inside which we handle the display of the image on the screen.

You probably think “That's great!” And you're right - fewer .then () blocks for wrapping the code, it all looks like synchronous code, so it is intuitive.

Add error handling


If you want to add error handling, you have several options.

You can use the synchronous try ... catch structure with async / await. This example is an extended version of the code above:

async function myFetch(){
    try{
        let response = await fetch('coffee.jpg')
        let myBlob = await response.blob()

        let objectURL = URL.createObjectURL(myBlob)
        let image = document.createElement('img')
        image.src = objectURL
        document.body.appendChild(image)
    } catch(e){
        console.log(e)
    }
}

myFetch()

The catch () {} block accepts an error object, which we named "e"; Now we can output it to the console, this will allow us to receive a message about where the error occurred in the code.

If you want to use the second version of the code shown above, you should simply continue to use the hybrid approach and add the .catch () block to the end of the .then () call, as follows:

async function myFetch(){
    let response = await fecth('coffee.jpg')
    return await response.blob()
}

myFetch().then((blob) => {
    let objectURL = URL.createObjectURL
    let image = document.createElement('img')
    image.src = objectURL
    document.body.appendChild(image)
}).catch(e => console.log(e))

This is possible because the .catch () block will catch errors that occur both in the asynchronous function and in the promise chain. If you use the try / catch block here, you will not be able to handle errors that occur when the myFetch () function is called.

You can find both examples on GitHub:
simple-fetch-async-await-try-catch.html (see source )

simple-fetch-async-await-promise-catch.html (see source )

Waiting for Promise.all ()


Async / await is based on promises, so you can take full advantage of the latter. These include Promise.all () in particular - you can easily add await to Promise.all () to write all the return values ​​in a way similar to synchronous code. Again, take the example from the previous article . Keep a tab open with it to compare with the code shown below.

With async / await (see live demo and source code ) it looks like this:

async function fetchAndDecode(url, type){
    let repsonse = await fetch(url)

    let content

    if(type === 'blob'){
        content = await response.blob()
    } else if(type === 'text'){
        content = await response.text()
    }

    return content
}

async function displayContent(){
    let coffee = fetchAndDecode('coffee.jpg', 'blob')
    let tea = fetchAndDecode('tea.jpg', 'blob')
    let description = fetchAndDecode('description.txt', 'text')

    let values = await Promise.all([coffee, tea, description])

    let objectURL1 = URL.createObjectURL(values[0])
    let objectURL2 = URL.createObjectURL(values[1])
    let descText = values[2]

    let image1 = document.createElement('img')
    let image2 = document.createElement('img')
    image1.src = objectURL1
    image2.src = objectURL2
    document.body.appendChild(image1)
    document.body.appendChild(image2)

    let para = document.createElement('p')
    para.textContent = descText
    document.body.appendChild(para)
}

displayContent()
.catch(e => console.log(e))

We easily made the fetchAndDecode () function asynchronous with a couple of changes. Pay attention to the line:

let values = await Promise.all([coffee, tea, description])

Using await, we get the results of three promises in the values ​​variable, in a way similar to synchronous code. We must wrap the entire function in a new asynchronous function, displayContent (). We did not achieve a strong code reduction, but were able to extract most of the code from the .then () block, which provides useful simplification and makes the code more readable.

To handle errors, we added a .catch () block to our call to displayContent (); It handles errors of both functions.

Remember: you can also use the .finally () block to get a report on the operation — you can see it in action in our live demo (see also the source code ).

Async / await disadvantages


Async / await has a couple of flaws.

Async / await makes the code look like synchronous and, in a sense, makes it behave more synchronously. The await keyword blocks the execution of the code following it until the promise is completed, as happens in a synchronous operation. This allows you to perform other tasks, but your own code is locked.

This means that your code can be slowed down by a large number of pending promises following one after another. Each await will wait for the completion of the previous one, while we would like the promises to be fulfilled simultaneously, as if we were not using async / await.

There is a design pattern to mitigate this problem - disabling all promise processes by storing Promise objects in variables and then waiting them. Let's look at how this is implemented.

We have two examples at our disposal: slow-async-await.html (see source code ) and fast-async-await.html (see source code ). Both examples start with a promise function that mimics an asynchronous operation using setTimeout ():

function timeoutPromise(interval){
    return new Promise((resolve, reject) => {
        setTimeout(function(){
            resolve('done')
        }, interval)
    })
}

Then follows the asynchronous function timeTest (), which expects three calls to timeoutPromise ():

async function timeTest(){
    ...
}

Each of the three calls to timeTest () ends with a record of the time it took to fulfill the promise, then the time it takes to complete the whole operation is recorded:

let startTime = Date.now()
timeTest().then(() => {
    let finishTime = Date.now()
    let timeTaken = finishTime - startTime
    alert('Time taken in milliseconds: ' + timeTaken)
})

In each case, the timeTest () function is different.

In slow-async-await.html timeTest () looks like this:

async function timeTest(){
    await timeoutPromise(3000)
    await timeoutPromise(3000)
    await timeoutPromise(3000)
}

Here we just expect three calls to timeoutPromise, each time setting a delay of 3 seconds. Each call waits for the completion of the previous one - if you run the first example, you will see a modal window in about 9 seconds.

In fast-async-await.html timeTest () looks like this:

async function timeTest(){
    const timeoutPromise1 = timeoutPromise(3000)
    const timeoutPromise2 = timeoutPromise(3000)
    const timeoutPromise3 = timeoutPromise(3000)

    await timeoutPromise1
    await timeoutPromise2
    await timeoutPromise3
}

Here we save three Promise objects in variables, which causes the processes associated with it to run simultaneously.

Further, we expect their results - as promises begin to be fulfilled simultaneously, promises will also be completed at the same time; when you run the second example, you will see a modal window in about 3 seconds!

You should carefully test the code and keep this in mind while reducing performance.

Another minor inconvenience is the need to wrap the expected promises in an asynchronous function.

Using async / await with classes


In conclusion, we note that you can add async even in methods for creating classes so that they return promises and wait for promises inside them. Take the code from the article about object-oriented JS and compare it with the version modified using async:

class Person{
    constructor(first, last, age, gender, interests){
        this.name = {
            first,
            last
        }
        this.age = age
        this.gender = gender
        this.interests = interests
    }

    async greeting(){
        return await Promise.resolve(`Hi! I'm ${this.name.first}`)
    }

    farewell(){
        console.log(`${this.name.first} has left the building. Bye for now!`)
    }
}

let han = new Person('Han', 'Solo', 25, 'male', ['Smuggling'])

The class method can be used as follows:

han.greeting().then(console.log)

Browser support


One of the obstacles to using async / await is the lack of support for older browsers. This feature is available in almost all modern browsers, as well as promises; Some problems exist in Internet Explorer and Opera Mini.

If you want to use async / await, but need the support of older browsers, you can use the BabelJS library - it allows you to use the latest JS, converting it to a suitable one for a particular browser.

Conclusion


Async / await allows you to write asynchronous code that is easy to read and maintain. Although async / await is worse supported than other ways to write asynchronous code, it is definitely worth exploring.

Thank you for attention.

Happy coding!

All Articles