Understanding Async/Await and Promises in JavaScript and TypeScript

mochafreddo

Geoffrey Kim

Posted on February 27, 2024

Understanding Async/Await and Promises in JavaScript and TypeScript

In the asynchronous world of JavaScript and TypeScript, understanding how to effectively work with Promises and the async/await syntax is crucial for developing efficient and readable code. This blog post aims to demystify the scenarios where you might or might not need to use await when dealing with Promises, through detailed explanations and illustrations.

Introduction to Promises and Async/Await

Before diving into the specifics, let's quickly recap what Promises and async/await are:

  • Promises are objects that represent the eventual completion (or failure) of an asynchronous operation, and its resulting value.
  • The async/await syntax is syntactic sugar built on top of Promises, making asynchronous code look and behave a little more like synchronous code, thus improving readability and manageability.

When You Don't Need to Use await with Promises

Directly Returning a Promise

When an async function is designed to perform an asynchronous operation without the need to manipulate the result within the same function, you can return a Promise directly. This approach chains the returned Promise to the caller, allowing them to handle the resolution or rejection.

Consider a function that fetches data from an API:

async function fetchData() {
  return fetch('https://api.example.com/data'); // Directly returning a Promise
}

// The caller handles the Promise
fetchData()
  .then(data => console.log(data))
  .catch(error => console.error(error));
Enter fullscreen mode Exit fullscreen mode

In this scenario, using await inside fetchData is unnecessary because we're not processing the data within the function. The direct return makes our intention clear and keeps the code concise.

Illustration

Imagine a relay race where the baton is the promise. The function fetchData is the first runner who hands off the baton (Promise) directly to the next runner (caller) without stopping. The caller then decides how to cross the finish line with it.

When You Need to Use await with Promises

Processing the Resolved Value

When your function must process the data returned by a Promise before sending it back, using await becomes essential. It pauses the function execution until the Promise resolves.

Let's enhance our previous example to parse the JSON response:

async function fetchData() {
  const response = await fetch('https://api.example.com/data'); // Await for the Promise to resolve
  const data = await response.json(); // Process the resolved value
  return data; // Return processed data
}

fetchData()
  .then(data => console.log(data))
  .catch(error => console.error(error));
Enter fullscreen mode Exit fullscreen mode

Here, await ensures that fetch completes, and we then wait again for the response.json() promise to resolve. Only after these asynchronous operations complete do we return the processed data.

Illustration

Continuing with the relay race analogy, fetchData now pauses to inspect the baton (await the Promise) and perhaps tape a message to it (process the data) before passing it on. This additional step ensures the next runner (caller) has all the information needed to finish the race successfully.

Exception Handling with Try/Catch

Using await within a try-catch block in asynchronous functions allows for elegant error handling. This is something you cannot achieve when directly returning a Promise without await.

async function fetchData() {
  try {
    const response = await fetch('https://api.example.com/data');
    const data = await response.json();
    return data;
  } catch (error) {
    console.error("Failed to fetch data:", error);
    throw error; // Rethrow after handling
  }
}
Enter fullscreen mode Exit fullscreen mode

Summary

  • Without await: Directly return a Promise when no processing is needed within the function, effectively passing the responsibility to the caller.
  • With await: Use it to process the resolved value of a Promise or for inline error handling, providing a clearer and more manageable flow.

Conclusion

Understanding when and how to use await with Promises in JavaScript and TypeScript is key to writing code that's not just functional but also clean and readable. Whether you're directly chaining promises or pausing execution with await to handle data, the choice depends on your specific scenario and what you're trying to achieve. By mastering these concepts, you can ensure your asynchronous code is both efficient and elegant.

💖 💪 🙅 🚩
mochafreddo
Geoffrey Kim

Posted on February 27, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related