DEV Community

Cover image for Mastering Javascript async/await - The Ultimate Guide for Developers
Christopher Glikpo  ⭐
Christopher Glikpo ⭐

Posted on

Mastering Javascript async/await - The Ultimate Guide for Developers

Asynchronous programming is an essential aspect of modern web development. It allows developers to write code that doesn't block the main thread and can handle multiple tasks simultaneously. However, writing asynchronous code can be challenging, and the traditional callback-based approach can lead to callback hell and difficult-to-read code.

To address these issues, JavaScript introduced the async/await feature. It is a syntactic sugar on top of the Promise API and provides an elegant way to write asynchronous code that is easy to read and maintain. In this blog, we will explore async/await in-depth and provide a comprehensive guide for developers looking to master this powerful feature.

Understanding Promises

Before we dive into async/await, let's quickly review Promises. Promises are objects that represent a value that may not be available yet. They are used to handle asynchronous operations and provide a clean and structured way to handle callbacks.A Promise is an object that represents the eventual completion or failure of an asynchronous operation and its resulting value. A Promise can be in one of three states:

  • Pending: The initial state of a Promise is "pending". This means that the Promise is neither fulfilled nor rejected. In this state, the Promise is still in progress, and the result of the asynchronous operation is not yet available.

  • Fulfilled: A Promise is "fulfilled" when the asynchronous operation has completed successfully, and the resulting value is available. In this state, the Promise has a value, and any then() callbacks registered on the Promise will be called with this value.

  • Rejected: A Promise is "rejected" when the asynchronous operation has completed with an error or a failure, and the reason for the failure is available. In this state, the Promise has a reason, and any catch() or finally() callbacks registered on the Promise will be called with this reason.

Creating a Promise

You can create a new Promise object using the Promise() constructor. The constructor takes a function as an argument, which is called the "executor" function. The executor function takes two arguments: resolve() and reject(). resolve() is used to fulfill the Promise with a value, and reject() is used to reject the Promise with a reason.

Here's an example of creating a new Promise:

const myPromise = new Promise((resolve, reject) => {
  // Perform an asynchronous operation
  const result = doSomethingAsynchronous();

  if (result) {
    resolve(result);
  } else {
    reject(new Error('Something went wrong!'));
  }
});
Enter fullscreen mode Exit fullscreen mode

In this example, we create a new Promise that performs an asynchronous operation using the doSomethingAsynchronous() function. If the operation is successful, we call resolve() with the resulting value. Otherwise, we call reject() with an error object.

Using then() and catch()

Once you have created a Promise, you can use the then() and catch() methods to register callbacks that will be called when the Promise is fulfilled or rejected, respectively.

Here's an example of using then() and catch() to handle a fulfilled Promise:

myPromise.then(result => {
  console.log(result); // Do something with the result
}).catch(error => {
  console.error(error); // Handle the error
});
Enter fullscreen mode Exit fullscreen mode

In this example, we register a then() callback that will be called when the Promise is fulfilled. If the Promise is rejected, the catch() callback will be called instead.

Chaining Promises with then()

Promises can also be chained together using the then() method. This allows you to perform a series of asynchronous operations in sequence.

Here's an example of chaining Promises:

fetch('https://example.com/data')
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error(error));
Enter fullscreen mode Exit fullscreen mode

In this example, we first make a network request using the fetch() function. We then call response.json() to extract the JSON data from the response. The resulting Promise is then chained with another then() method, where we log the JSON data to the console. If an error occurs during the network request or the JSON parsing, the catch() method will be called to handle the error.

Introducing async/await

Async/await is a feature that was introduced in ECMAScript 2017. It allows you to write asynchronous code in a synchronous style, making it easier to read and write.

The async keyword is used to define a function as asynchronous. This means that the function returns a Promise, even if it doesn't explicitly use the Promise API. The await keyword is used to pause the execution of the function until the Promise is fulfilled or rejected.

Async/await syntax

To use async/await, you define an asynchronous function using the async keyword. Inside the function, you use the await keyword to wait for a Promise to resolve. Here's an example:

async function fetchData() {
  const response = await fetch('https://api.example.com/data');
  const data = await response.json();
  return data;
}
Enter fullscreen mode Exit fullscreen mode

In this example, the fetchData() function is defined as asynchronous using the async keyword. The function uses the fetch() method to make an HTTP request to the API endpoint. The await keyword is used to pause the execution of the function until the Promise returned by fetch() is fulfilled. Once the Promise is fulfilled, the function continues executing, and the data is extracted from the response using the json() method.

Error handling

Async/await provides a structured way to handle errors. You can use try/catch blocks to catch and handle errors in a synchronous style. Here's an example:

async function fetchData() {
  try {
    const response = await fetch('https://api.example.com/data');
    const data = await response.json();
    return data;
  } catch (error) {
    console.error(error);
    return null;
  }
}
Enter fullscreen mode Exit fullscreen mode

In this example, the fetchData() function uses a try/catch block to catch any errors that occur during the execution of the function. If an error occurs, the catch block is executed, and the error is logged to the console. The function returns null in this case.

Parallel execution

Async/await allows you to execute multiple asynchronous operations in parallel. You can use Promise.all() to wait for multiple Promises to resolve before continuing execution. Here's an example:

async function fetchAllData() {
  const [data1, data2, data3] = await Promise.all([
    fetchData('https://api.example.com/data1'),
    fetchData('https://api.example.com/data2'),
    fetchData('https://api.example.com/data3')
  ]);
  return { data1, data2, data3 };
}
Enter fullscreen mode Exit fullscreen mode

In this example, the fetchAllData() function uses Promise.all() to wait for three asynchronous operations to complete. Each asynchronous operation is represented by a call to the fetchData() function, which returns a Promise. The Promise.all() method waits for all three Promises to resolve before continuing execution.

Sequential execution

Async/await also allows you to execute asynchronous operations sequentially. You can use the await keyword to wait for each asynchronous operation to complete before moving on to the next one. Here's an example:

async function fetchSequentialData() {
  const data1 = await fetchData('https://api.example.com/data1');
  const data2 = await fetchData('https://api.example.com/data2');
  const data3 = await fetchData('https://api.example.com/data3');
  return { data1, data2, data3 };
}
Enter fullscreen mode Exit fullscreen mode

In this example, the fetchSequentialData() function executes three asynchronous operations sequentially. Each operation is represented by a call to the fetchData() function, and the await keyword is used to wait for each operation to complete before moving on to the next one.

Async functions and Promises

Async functions are built on top of Promises. When you define an async function, it returns a Promise, even if you don't explicitly use the Promise API. Here's an example:

async function foo() {
  return 'hello';
}

foo().then(result => {
  console.log(result); // 'hello'
});
Enter fullscreen mode Exit fullscreen mode

In this example, the foo() function returns the string 'hello'. When the function is called, it returns a Promise that resolves to 'hello'. The then() method is used to handle the resolved value of the Promise.

Async/await and callbacks

Async/await provides a more readable and structured way to work with asynchronous code compared to using callbacks. You can convert a function that uses callbacks to an async function by wrapping the callback in a Promise. Here's an example:

function fetchData(callback) {
  fetch('https://api.example.com/data')
    .then(response => response.json())
    .then(data => callback(data))
    .catch(error => console.error(error));
}

async function fetchDataAsync() {
  try {
    const response = await fetch('https://api.example.com/data');
    const data = await response.json();
    return data;
  } catch (error) {
    console.error(error);
    return null;
  }
}
Enter fullscreen mode Exit fullscreen mode

In this example, the fetchData() function uses callbacks to handle the asynchronous response. The fetchDataAsync() function uses async/await to achieve the same result. The fetch() method returns a Promise, which is resolved with the response object. The json() method also returns a Promise, which is resolved with the parsed JSON data. The error handling is also more structured and easier to read in the async/await version.

Allows for better control flow

Async/await allows for better control flow by making it easier to handle dependencies between asynchronous operations. For example, you can use the await keyword to wait for one operation to complete before starting another.

async function getData() {
  const userResponse = await fetch('https://example.com/users');
  const userData = await userResponse.json();
  const userId = userData[0].id;

  const postResponse = await fetch(`https://example.com/posts?userId=${userId}`);
  const postData = await postResponse.json();

  return { user: userData[0], posts: postData };
}
Enter fullscreen mode Exit fullscreen mode

In this example, we use the await keyword to wait for the first fetch request to complete before extracting the user ID and using it in the second fetch request. This allows us to control the order of execution and ensure that we have the necessary data before proceeding.

Conclusion

Async/await is a powerful feature of JavaScript that simplifies working with asynchronous code. It provides a more structured and readable way to handle Promises and error handling. It also allows you to execute asynchronous operations in parallel or sequentially, depending on your needs. By mastering async/await, you can write cleaner and more maintainable asynchronous code in your JavaScript projects.

Top comments (0)