JavaScript async/await: A Comprehensive Tutorial

In modern JavaScript, async/await provides a cleaner and more readable way to work with asynchronous code compared to traditional callback functions or .then() chains with Promises.

Introduced in ES2017, async and await allow you to write asynchronous code in a style that looks synchronous, making it easier to follow and maintain.

This tutorial will guide you through the basics of async and await, with several examples demonstrating how to use them effectively in JavaScript.

1. Basic Concept of async and await

async function: A function declared with the async keyword automatically returns a promise. The value that is returned from the function is wrapped in a resolved promise.
await expression: Pauses the execution of an async function until the promise settles (either resolved or rejected). It allows you to work with the resolved value of a promise directly, without using .then().

Example 1: Basic async Function

async function sayHello() {
  return 'Hello!';
}

sayHello().then((message) => {
  console.log(message); // Output: "Hello!"
});

Explanation:

The sayHello() function is marked with the async keyword, so it automatically returns a promise that resolves to the value ‘Hello!'.
We use .then() to handle the resolved value of the promise.

2. Using await to Wait for Promises

The await keyword allows you to wait for a promise to resolve before proceeding with the rest of the code. It can only be used inside an async function.

Example 2: Using await with a Promise

function resolveAfter2Seconds() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve('Resolved after 2 seconds');
    }, 2000);
  });
}

async function asyncCall() {
  console.log('Calling...');
  const result = await resolveAfter2Seconds();
  console.log(result); // Output: "Resolved after 2 seconds"
}

asyncCall();

Explanation:

resolveAfter2Seconds() returns a promise that resolves after 2 seconds.
In asyncCall(), we use await to pause the execution until the promise resolves, allowing us to use the resolved value directly (result).

3. Error Handling with try…catch

With async/await, you can handle errors using try…catch blocks. This is a cleaner way to catch errors compared to using .catch() with promises.

Example 3: Error Handling with async/await

function fetchData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      reject('Error: Failed to fetch data');
    }, 1000);
  });
}

async function getData() {
  try {
    const data = await fetchData();
    console.log(data);
  } catch (error) {
    console.log(error); // Output: "Error: Failed to fetch data"
  }
}

getData();

Explanation:

fetchData() returns a promise that rejects after 1 second.
We use try…catch inside the async function getData() to handle the error when fetchData() rejects.

4. Using async/await with Multiple Promises

You can use await multiple times in a single async function to wait for several promises to resolve. Each await pauses the execution until the respective promise resolves.

Example 4: Handling Multiple Promises Sequentially

function wait(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

async function processSteps() {
  console.log('Step 1');
  await wait(1000); // Wait for 1 second

  console.log('Step 2');
  await wait(2000); // Wait for 2 seconds

  console.log('Step 3');
}

processSteps();
// Output: 
// Step 1
// (wait 1 second)
// Step 2
// (wait 2 seconds)
// Step 3

Explanation:

We create a wait() function that returns a promise that resolves after a certain number of milliseconds.
In processSteps(), we sequentially wait for each step by using await, causing the code to pause for the specified amount of time before proceeding to the next step.

5. Parallel Execution with Promise.all()

If you want multiple promises to execute in parallel rather than sequentially, you can use Promise.all() along with await. This method waits for all the promises to resolve but runs them in parallel.

Example 5: Running Promises in Parallel with Promise.all()

function wait(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

async function runInParallel() {
  const promise1 = wait(1000);
  const promise2 = wait(2000);
  const promise3 = wait(1500);

  const results = await Promise.all([promise1, promise2, promise3]);
  console.log('All promises resolved');
}

runInParallel();
// Output after 2 seconds: "All promises resolved"

Explanation:

Three promises (promise1, promise2, and promise3) are created, each waiting for different durations.
Promise.all() is used to run them in parallel and wait for all of them to resolve before proceeding.

6. Returning Values from async Functions

An async function always returns a promise. You can return any value, and it will be wrapped in a resolved promise automatically.

Example 6: Returning Values from async Functions

async function addNumbers(a, b) {
  return a + b;
}

addNumbers(3, 7).then((result) => {
  console.log(result); // Output: 10
});

Explanation:

The addNumbers() function returns the sum of two numbers, and since it’s marked async, the result is automatically wrapped in a promise.
We use .then() to access the resolved value of the promise.

7. Handling Async Iteration with for…of

You can iterate over asynchronous operations using for…of combined with await.

Example 7: Iterating over Asynchronous Operations

function fetchData(id) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(`Data for ID: ${id}`);
    }, 1000 * id); // Each request takes a different amount of time
  });
}

async function processAllData() {
  const ids = [1, 2, 3];
  for (const id of ids) {
    const data = await fetchData(id);
    console.log(data); // Logs each result after the corresponding fetch completes
  }
}

processAllData();
// Output:
// Data for ID: 1
// Data for ID: 2
// Data for ID: 3

Explanation:

We loop through an array of ids and await the result of fetchData() for each id.
The await inside the loop ensures that each fetchData() completes before moving on to the next iteration.

8. Combining async/await with Promise.race()

Promise.race() returns the result of the first promise that resolves (or rejects). You can combine this with await for scenarios where you want the fastest result.

Example 8: Using Promise.race() with async/await

function slowPromise() {
  return new Promise((resolve) => setTimeout(() => resolve('Slow'), 3000));
}

function fastPromise() {
  return new Promise((resolve) => setTimeout(() => resolve('Fast'), 1000));
}

async function racePromises() {
  const result = await Promise.race([slowPromise(), fastPromise()]);
  console.log(result); // Output: "Fast"
}

racePromises();

Explanation:

Promise.race() waits for the first promise to resolve or reject.
In this case, fastPromise() resolves before slowPromise(), so the result is ‘Fast'.

Conclusion

The async/await syntax provides a modern and cleaner way to handle asynchronous code in JavaScript. By using await, you can write code that looks synchronous but still executes asynchronously, making it easier to understand and maintain.

With try…catch blocks, handling errors is also more straightforward compared to traditional promise chains.

Key Points:

async function: Always returns a promise.
await expression: Pauses the execution of an async function until the promise is resolved.
Error handling: Use try…catch to handle errors in async/await code.
Multiple promises: Use Promise.all() to run promises in parallel and Promise.race() to get the result of the fastest promise.
Returning values: An async function wraps the returned value in a resolved promise automatically.

Experiment with these examples to get comfortable with async/await in your JavaScript code!

Related posts

JavaScript Template Literals Tutorial with Examples

JavaScript Reflect: A Comprehensive Tutorial

JavaScript Proxy: A Comprehensive Tutorial