Async/await: a modern syntax for asynchronous JavaScript

Async-await syntax was introduced to JavaScript in ES2017 as a way to handle JavaScript promises in a way that resembles synchronous code. Under the hood, as we will see in this tutorial, async-await is powered by promises.

It does so by introducing two new keywords: async and await.

Async-await syntax

The async keyword

The async keyword allows us to transform a regular function to one the returns a promise by default.

No writing of promises in necessary: just append the keyword async before a function and it will wrap its contents within a promise.

For example, look how we can handle the result of myAsyncFunction using promise syntax:

async function myAsyncFunction () {
   return 2+2;
}

myAsyncFunction().then(res => console.log(res)); // 4

This is the first feature of the async keyword. It's second feature is that enables the use of the await keyword within a function prepended by async.

The await keyword

The await keyword can be used within a function marked as async. When prepended before a promise, it awaits its outcome.

For example:

const myPromise1 = new Promise(function(resolve, reject) { 
  setTimeout(function(){
    resolve("First task");
    },2000);
});

async function myAsyncFunction() {
  const result = await myPromise1;
  console.log(result);
}

myAsyncFunction();

Remove the await keyword in front of myPromise1 and the console.log(result) would return Promise { <pending> } because the result was attempted to be accessed too soon.

It is important to know that the use of the await keyword pauses further execution of the contents of the function until a process is complete.

This can be seen clearly by running the below example, in which we await one promise after another:

const myPromise1 = new Promise(function(resolve, reject) { 
  setTimeout(function(){
    resolve("First task");
    },1000);
});
  
const myPromise2 = new Promise(function(resolve, reject) { 
  setTimeout(function(){
    resolve("Second task");
    },2000); 
});
  
const myPromise3 = new Promise(function(resolve, reject) { 
  setTimeout(function(){
    resolve("Third task");
    },3000); 
});

async function myAsyncFunction() {
  console.log("Here we go!")
  const result1 = await myPromise1;
  console.log("First promise done");
  const result2 = await myPromise2;
  console.log("Second one down");
  const result3 = await myPromise3;
  console.log("Another one bites the dust!");
}

myAsyncFunction();
// "Here we go!"..."First promise done"..."Second one down"..."Another one bites the dust!"

But this is not serial processing because JavaScript engine starts executing the contents of a promise immediately (unless it is wrapped in a function). So if all promises took the same amount of time to complete, all promises would return results at the same because there is nothing to wait for after the first promise is complete:

const myPromise1 = new Promise(function(resolve, reject) { 
  setTimeout(function(){
    resolve("First task");
    },2000);
});
  
const myPromise2 = new Promise(function(resolve, reject) { 
  setTimeout(function(){
    resolve("Second task");
    },2000); 
});
  
const myPromise3 = new Promise(function(resolve, reject) { 
  setTimeout(function(){
    resolve("Third task");
    },2000); 
});

async function myAsyncFunction() {
  console.log("Here we go!")
  const result1 = await myPromise1;
  console.log("First promise done");
  const result2 = await myPromise2;
  console.log("Second one down");
  const result3 = await myPromise3;
  console.log("Another one bites the dust!");
}

myAsyncFunction();
// Immediately: "Here we go!"
// After 2 seconds: "First promise done","Second one down","Another one bites the dust!"

Error-handling

To handle a potential error in an async function, we can use try...catch syntax.

We first wrap our asynchronous function code in a try statement. We then follow it with a catch statement and do our error-handling there:

const myPromise1 = new Promise(function(resolve, reject) { 
  setTimeout(function(){
    resolve("First task");
    },2000);
});
  
const myPromise2 = new Promise(function(resolve, reject) { 
  setTimeout(function(){
    reject("Promise rejected!");
    resolve("Second task");
    },2000); 
});
  
const myPromise3 = new Promise(function(resolve, reject) { 
  setTimeout(function(){
    resolve("Third task");
    },2000); 
});

async function myAsyncFunction() {
  try {
  const result1 = await myPromise1;
  const result2 = await myPromise2;
  const result3 = await myPromise3;
  } catch(err) {
    console.log("Houston, we have a problem: " + err);
  }
}

myAsyncFunction();
// Houston, we have a problem: Promise rejected!

The catch statement works just like .catch in a .then chain: if there is an error in our asynchronous function statement, processing of the try statement will stop and the catch statement will be triggered.

An example using Fetch

Under the hood, async-await works with promises. So any async-await syntax can be rewritten with promises syntax and vice-versa.

Here is the way the result of a fetch request would normally be dealt with using ES6 promises syntax:

function withPromiseSyntax() {
  fetch('https://httpbin.org/get')
    .then(res => res.json())
    .then(data => console.log(data))
    .catch(error => console.log(error))
}

withPromiseSyntax()

And now with async-await:

async function withAsyncAwait() {
  try {
    const res = await fetch('https://httpbin.org/get');
    const data = await res.json();
    console.log(data);
  } catch(err) {
    console.log(err);
  }
}

withAsyncAwait();

Both code snippets achieve the same outcome. However, except for the async and await keywords, the async-await syntax strongly resembles synchronous code.

Async-await loop

Since ES2018 we can execute promises serially using an async...for await loop.

Promises are loaded into anonymous functions (so they do not execute immediately) and stored in an array.

Now, a for...of loop is created inside an async function. With the loop, each function in the array is called, prepended by the keyword await. This make the loop await the result of each function call before moving on to the next:

// Create an array of promises
const myPromises = [
    _ => new Promise(resolve => setTimeout(() => 
    {return resolve("First task")}, 
    1000)),
    _ => new Promise(resolve => setTimeout(() => 
    {return resolve("Second task")}, 
    1000)),
    _ => new Promise(resolve => setTimeout(() => 
    {return resolve("Third task")}, 
    1000)),
  ];
  
// Create the async function
async function executeMyPromises(myPromises) {
    for (let promise of myPromises) {
        console.log(await promise());
    }
};
  
// Run function
executeMyPromises(myPromises);
// "First task"..."Second task"..."Third task"

Combining async-await with Promise object methods

The async-await syntax can be combined with methods available on the Promise object to flexibly work the results of multiple promises.

For example, Promise.all returns the results of all promises together when all are complete:

const myPromise1 = new Promise(function(resolve, reject) { 
  setTimeout(function(){
    resolve("First task");
    },2000);
});
  
const myPromise2 = new Promise(function(resolve, reject) { 
  setTimeout(function(){
    resolve("Second task");
    },2000); 
});
  
const myPromise3 = new Promise(function(resolve, reject) { 
  setTimeout(function(){
    resolve("Third task");
    },2000); 
});

async function myAsyncFunction() {
  try {
  const result = await Promise.all([
    myPromise1, myPromise2, myPromise3]);
    console.log(result);
  } catch(err) {
    console.log("Houston, we have a problem: " + err);
  }
}

myAsyncFunction();
// Output after 2 seconds: 
// ["First task","Second task","Third task"]

It is possible to replace Promise.all in the above code with Promise.allSettled, Promise.any or Promise.race to process the results differently.

See our post on handling multiple promises in JavaScript for a breakdown of these methods.

Summary

Async-await syntax takes JavaScript full circle: from synchronous code to callbacks, callbacks to promises and now, with async-await, our code can look and read like synchronous code again.

It is likely that future updates build upon – rather than revolutionize – this framework.

Leave a Reply

Your email address will not be published.