JavaScript Async/Await: A Guide to Asynchronous Programming

Introduction

Asynchronous programming is a way of writing code that allows multiple tasks to run concurrently without blocking the main thread. This is useful for performing operations that take some time to complete, such as fetching data from a server, reading a file, or processing an image.

However, asynchronous programming can also be challenging, especially when dealing with multiple dependent tasks that need to be executed in a certain order. For example, suppose you want to fetch some data from a server, then process it, then display it on the web page. How do you ensure that each step is completed before moving on to the next one, without creating a complex and nested callback structure?

Using promises

One solution is to use promises, which are objects that represent the eventual completion or failure of an asynchronous operation. Promises allow you to write asynchronous code in a more readable and modular way, by chaining then() and catch() methods that handle the fulfillment or rejection of the promise. For example, the following code uses promises to fetch and display some data:

Javascript
                        
// Fetch some data from a server
fetch('/data.json')
  // Convert the response to JSON
  .then(response => response.json())
  // Process the data
  .then(data => {
    // Do something with the data
    console.log(data);
    // Return the processed data
    return data;
  })
  // Display the data on the web page
  .then(data => {
    // Create an element to display the data
    let element = document.createElement('div');
    // Set the element's content to the data
    element.textContent = JSON.stringify(data);
    // Append the element to the document body
    document.body.appendChild(element);
  })
  // Handle any errors
  .catch(error => {
    // Log the error
    console.error(error);
    // Display an error message on the web page
    alert('Something went wrong!');
  });

However, promises are not perfect. They can still result in verbose and confusing code, especially when dealing with complex logic or error handling. For example, suppose you want to fetch data from multiple sources, then combine them, then display them on the web page. How do you handle the cases where some of the fetches fail, or where the data is not compatible, or where the display fails?

Another solution is to use async/await, which is a syntactic sugar that makes working with promises easier and more elegant. Async/await allows you to write asynchronous code as if it were synchronous, by using the async and await keywords. For example, the following code uses async/await to fetch and display some data:

Javascript
                        
// Define an async function
async function fetchDataAndDisplay() {
  try {
    // Fetch some data from a server
    let response = await fetch('/data.json');
    // Convert the response to JSON
    let data = await response.json();
    // Process the data
    // Do something with the data
    console.log(data);
    // Create an element to display the data
    let element = document.createElement('div');
    // Set the element's content to the data
    element.textContent = JSON.stringify(data);
    // Append the element to the document body
    document.body.appendChild(element);
  } catch (error) {
    // Handle any errors
    // Log the error
    console.error(error);
    // Display an error message on the web page
    alert('Something went wrong!');
  }
}

// Call the async function
fetchDataAndDisplay();

As you can see, the async/await code is much simpler and cleaner than the promise code. It looks like regular synchronous code, but it still runs asynchronously under the hood. This makes it easier to read, write, and maintain.

In this article, we will explain how async/await works, how to use it, and what are its benefits and drawbacks. We will also show some examples of common use cases and best practices for async/await.

Async Functions

The async keyword is used to declare an async function, which is a function that returns a promise. The async keyword can be used before a function declaration, a function expression, an arrow function, or a method. For example:

Async function declaration

Javascript
                        
// Async function declaration
async function foo() {
  // Do something asynchronous
}

Async function expression

Javascript
                        
// Async function expression
let bar = async function() {
  // Do something asynchronous
};
Javascript
                        
// Async arrow function
let baz = async () => {
  // Do something asynchronous
};
Javascript
                        
// Async method
let obj = {
  async qux() {
    // Do something asynchronous
  }
};

An async function can contain zero or more await expressions, which pause the execution of the function until the awaited promise is fulfilled or rejected. The value of the await expression is the fulfilled value of the promise, or the rejected value if the promise is rejected. For example:

Javascript
                        
// Define an async function
async function foo() {
  // Await a promise that resolves in 1 second
  let result = await new Promise(resolve => setTimeout(() => resolve('Hello'), 1000));
  // Log the result
  console.log(result); // Hello
}

// Call the async function
foo();

An async function always returns a promise, even if the return value is not explicitly a promise. The promise is resolved with the return value of the function, or rejected with the thrown error if the function throws an error. For example:

Javascript
                        
// Define an async function
async function foo() {
  // Return a non-promise value
  return 42;
}

// Call the async function
foo().then(result => console.log(result)); // 42

// Define another async function
async function bar() {
  // Throw an error
  throw new Error('Oops');
}

// Call the async function
bar().catch(error => console.error(error)); // Error: Oops

Note that the async keyword only affects the behavior of the function itself, not the caller of the function. The caller of the async function still needs to use then() or await to handle the returned promise. For example:

Javascript
                        
// Define an async function
async function foo() {
  // Return a value
  return 'Hello';
}

// Call the async function without then() or await
let result = foo();
// Log the result
console.log(result); // Promise {<fulfilled>: "Hello"}

// Call the async function with then()
foo().then(result => console.log(result)); // Hello

// Call the async function with await (inside another async function)
async function bar() {
  let result = await foo();
  console.log(result); // Hello
}

bar();

Await Expressions

The await keyword is used to pause the execution of an async function until the awaited promise is fulfilled or rejected. The await keyword can only be used inside an async function, otherwise it will cause a syntax error. The await keyword can be used with any expression that returns a promise, such as a function call, a variable, or a literal. For example:

Javascript
                        
// Define an async function
async function foo() {
  // Define a promise
  let promise = new Promise(resolve => setTimeout(() => resolve('Hello'), 1000));
  // Await the promise
  let result = await promise;
  // Log the result
  console.log(result); // Hello
}

// Call the async function
foo();

The await keyword can also be used with other async functions, creating a chain of async/await calls. For example:

Javascript
                        
// Define an async function that returns a promise
async function foo() {
  return new Promise(resolve => setTimeout(() => resolve('Hello'), 1000));
}

// Define another async function that calls the first one
async function bar() {
  // Await the result of foo()
  let result = await foo();
  // Log the result
  console.log(result); // Hello
}

// Call the async function
bar();

The await keyword can also be used with other types of expressions, such as arithmetic, logical, or conditional expressions. However, the expression must still return a promise, otherwise the await keyword will have no effect. For example:

Javascript
                        
// Define an async function
async function foo() {
  // Await an arithmetic expression that returns a promise
  let result = await (2 + 2) * new Promise(resolve => setTimeout(() => resolve(10), 1000));
  // Log the result
  console.log(result); // 40
}

// Call the async function
foo();

The await keyword can also be used in a loop, such as a for, while, or do...while loop. This allows you to iterate over an array of promises, or perform a repeated task until a condition is met. For example:

Javascript
                        
// Define an async function
async function foo() {
  // Define an array of promises
  let promises = [
    new Promise(resolve => setTimeout(() => resolve(1), 1000)),
    new Promise(resolve => setTimeout(() => resolve(2), 2000)),
    new Promise(resolve => setTimeout(() => resolve(3), 3000))
  ];
  // Use a for loop to await each promise
  for (let promise of promises) {
    // Await the promise
    let result = await promise;
    // Log the result
    console.log(result); // 1, 2, 3
  }
}

// Call the async function
foo();

The await keyword can also be used in a try...catch block, which allows you to handle errors that may occur during the execution of the async function. The try block contains the await expressions that may throw an error, and the catch block handles the error if it occurs. For example:

Javascript
                        
// Define an async function
async function foo() {
  try {
    // Await a promise that rejects in 1 second
    let result = await new Promise((resolve, reject) => setTimeout(() => reject('Oops'), 1000));
    // This line will not be executed
    console.log(result);
  } catch (error) {
    // Handle the error
    console.error(error); // Oops
  }
}

// Call the async function
foo();

The try...catch block can also be used to handle errors from other async functions that are called with await. For example:

Javascript
                        
// Define an async function that returns a rejected promise
async function foo() {
  return new Promise((resolve, reject) => setTimeout(() => reject('Oops'), 1000));
}

// Define another async function that calls the first one
async function bar() {
  try {
    // Await the result of foo()
    let result = await foo();
    // This line will not be executed
    console.log(result);
  } catch (error) {
    // Handle the error
    console.error(error); // Oops
  }
}

// Call the async function
bar();

The try...catch block can also be used to handle errors from other types of expressions that are awaited, such as arithmetic, logical, or conditional expressions. However, the expression must still throw an error, otherwise the catch block will not be executed. For example:

Javascript
                        
// Define an async function
async function foo() {
  try {
    // Await an arithmetic expression that throws an error
    let result = await (2 + 2) / new Promise((resolve, reject) => setTimeout(() => reject(0), 1000));
    // This line will not be executed
    console.log(result);
  } catch (error) {
    // Handle the error
    console.error(error); // 0
  }
}

// Call the async function
foo();

Benefits of Async/Await

Async/await is a powerful and elegant way of writing asynchronous code with promises. It has several benefits, such as:

  • It makes the code more readable and understandable, by avoiding the callback hell and the promise chaining.
  • It makes the code more concise and clean, by reducing the boilerplate code and the indentation levels.
  • It makes the code more consistent and predictable, by using the same syntax and logic as synchronous code.
  • It makes the code more robust and reliable, by using the standard try...catch block to handle errors.

Drawbacks of async/await

However, async/await also has some drawbacks, such as:

  • It is not supported by all browsers and environments, and may require transpilation or polyfills to work properly.
  • It may cause performance issues or memory leaks, if not used carefully and correctly.
  • It may introduce some complexity and confusion, if not understood well and used appropriately.

Best practices

Therefore, it is important to use async/await wisely and responsibly, and to follow some best practices, such as:

  • Use async/await only when necessary and beneficial, and do not overuse it or abuse it.
  • Use async/await only with promises, and do not mix it with other types of callbacks or asynchronous mechanisms.
  • Use async/await only inside async functions, and do not use it outside of them or in regular functions.
  • Use await only with expressions that return a promise, and do not use it with expressions that return other values or do not return anything.
  • Use try...catch blocks to handle errors from await expressions, and do not ignore or swallow them.
  • Use Promise.all() or Promise.race() to await multiple promises concurrently, and do not use await in a loop or a sequence unnecessarily.
  • Use return or throw statements to resolve or reject the promise returned by an async function, and do not use other methods or mechanisms.

Conclusion

Async/await is a syntactic sugar that makes working with promises easier and more elegant. It allows you to write asynchronous code as if it were synchronous, by using the async and await keywords. Async/await has many benefits, such as improving the readability, conciseness, consistency, and reliability of the code. However, async/await also has some drawbacks, such as requiring transpilation or polyfills, causing performance or memory issues, and introducing some complexity or confusion. Therefore, it is important to use async/await wisely and responsibly, and to follow some best practices.