async/await

async/await

Async/await simplifies asynchronous programming by allowing developers to write code that looks and behaves like synchronous code, improving readability and understandability.

This page is a follow-up branch of the in-depth article "Async in JS: How does it work", if you'd like to learn about the history of asynchronous JavaScript and its evolution, refer to it.

In a nutshell

When you use the async keyword before a function definition, it indicates that the function will be asynchronous. This means that the function is designed to work with asynchronous operations, such as network requests or reading files, without blocking the main execution thread. In JavaScript, asynchronous functions are usually implemented using promises.

As a result, when you declare a function as async, the return value of that function is automatically wrapped in a Promise. Even if the return value is not a promise itself, JavaScript will create a promise that resolves to that value.

Here's an example:

// An asynchronous function
async function getUserName(userId) {
  // Simulate an asynchronous operation (e.g., fetching data from a server)
  const simulatedData = {
    1: "Alice",
    2: "Bob",
    3: "Charlie",
  };

  return simulatedData[userId];
}

// Usage
getUserName(1).then((name) => {
  console.log(`User 1: ${name}`); // Output: User 1: Alice
});

In this example, the getUserName function is declared as async. Even though it returns a simple value from the simulatedData object, the return value is automatically wrapped in a promise. When we call the getUserName function, we can use the then method to handle the resolved value, just like we would with any other promise.


Asynchronous functions are essential when dealing with operations that take an unpredictable amount of time to complete, such as making API calls, interacting with databases, or reading files. These operations are not executed immediately; instead, the code sends a request and waits for the response.

The await keyword is used inside an async function to pause the execution of the function until the asynchronous operation is completed. It allows you to write code that appears synchronous, even though it's performing asynchronous tasks. This makes the code more readable and easier to understand.

// Import the 'fs' module to work with the file system
const fs = require('fs').promises;

// An async function to read a file
async function readFileContent(filePath) {
  try {
    // Use the 'await' keyword to wait for the file to be read
    const content = await fs.readFile(filePath, 'utf-8');
    console.log(`File content: ${content}`);
  } catch (error) {
    console.error(`Error reading the file: ${error.message}`);
  }
}

// Usage
readFileContent('./example.txt');

In this example, we're using the fs.promises module to read a file. The readFile function is asynchronous, so it returns a promise. Inside the readFileContent async function, we use the await keyword to wait for the fs.readFile promise to resolve. Once the promise resolves, the file's content is logged to the console. If there's an error reading the file, the catch block logs the error message.

Call chains vs async/await

The async/await keywords in JavaScript don't introduce new functionality; they simply make it easier to work with promises. They allow you to write cleaner and more intuitive code compared to using chained then and catch callbacks.

Here's an example illustrating the difference between using promises with a call chain and using async/await.

Using a call chain with promises:

function fetchData() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve("Data fetched");
    }, 1000);
  });
}

function processData(data) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(`${data} and processed`);
    }, 1000);
  });
}

// Chained promises with then and catch
fetchData()
  .then((data) => {
    return processData(data);
  })
  .then((result) => {
    console.log(result); // Output: Data fetched and processed
  })
  .catch((error) => {
    console.error(`Error: ${error}`);
  });

Using async/await:

async function fetchDataAndProcess() {
  try {
    const data = await fetchData();
    const result = await processData(data);
    console.log(result); // Output: Data fetched and processed
  } catch (error) {
    console.error(`Error: ${error}`);
  }
}

// Call the async function
fetchDataAndProcess();

In both examples, we fetch and process data using promises. The first example uses a call chain with then and catch, while the second example uses the async/await keywords. The async/await version is easier to understand because it looks more like synchronous code. It's also more straightforward to handle errors using try/catch blocks, making it a popular choice for developers working with asynchronous operations.

Tips & best practices

Below you'll find some personal tips but also industry best practices when working with async/await.

  • Always use async/await instead of then() chains and callbacks.

    • Async/await is easier to read, debug, and manage compared to chaining multiple then() and catch() functions or using callback functions.
  • Await should not be used outside of asynchronous functions.

    • The await keyword can only be used inside an async function. If you want to perform an asynchronous operation in the global scope, you must use then().
  • Use await Promise.all([...]) to execute several independent asynchronous functions in parallel.

    • Instead of waiting for one asynchronous function to complete before executing the next, you can run multiple independent async functions concurrently using Promise.all(). This can help improve the overall performance of your code.

Example:

async function getUser() {
      // Returns the information about the user
    }

    async function getNews() {
      // Returns the list of news
    }

    // Using Promise.all to run getUser() and getNews() concurrently
    const [user, news] = await Promise.all([getUser(), getNews()]);
  • Don't mix async/await and Promise.then syntax.

    • To maintain consistency and readability in your code, try to stick to one approach throughout your project: either async/await or Promise.then. Mixing the two can make your code harder to read and maintain.
  • Use try/catch blocks for error handling.

    • When using async/await, it's a good practice to handle errors with try/catch blocks. This approach is more consistent and easier to understand compared to handling errors with Promise.catch().

Example:

async function fetchData() {
      try {
        const data = await fetchSomeData();
        console.log("Data fetched:", data);
      } catch (error) {
        console.error("Error fetching data:", error);
      }
    }
  • Avoid "async/await hell."

    • Similar to "callback hell," "async/await hell" refers to a situation where you have multiple nested async functions, making the code hard to read and maintain. Instead, try to break down your code into smaller, reusable async functions.
  • Keep your asynchronous functions focused.

    • Try to create asynchronous functions that perform a single, well-defined task. This approach makes your code easier to understand, test, and maintain.
  • Use Promise.race([...]) when you need the result of the fastest promise.

    • If you have several asynchronous operations and need to get the result of the one that completes first, use Promise.race(). It returns a promise that resolves or rejects as soon as one of the input promises settles.

Example:

async function fetchFromAPI_A() {
      // Fetch data from API A
    }

    async function fetchFromAPI_B() {
      // Fetch data from API B
    }

    // Using Promise.race to get the result from the fastest API
    const fastestResult = await Promise.race([fetchFromAPI_A(), fetchFromAPI_B()]);

By adhering to these recommendations, you can improve the readability, maintainability, and performance of your JavaScript code when working with asynchronous operations using async/await and promises.