async functions
await
JavaScript
programming
asynchronous programming

Async functions vs await

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

JavaScript relies heavily on asynchronous operations for tasks like fetching data from servers, reading files, and running timers. Before async/await, developers managed these operations with callbacks and .then() chains, which often became deeply nested and hard to follow. The async and await keywords, introduced in ES2017, let you write asynchronous code that reads almost like synchronous code, making it far easier to reason about.

Async Functions Return Promises

Every function declared with the async keyword automatically wraps its return value in a Promise. This means callers can use .then() on the result or await it from another async function.

javascript
1async function greet() {
2  return "Hello, world!";
3}
4
5// The return value is wrapped in a resolved Promise
6greet().then((message) => console.log(message)); // "Hello, world!"
7
8// Equivalent to writing:
9function greetManual() {
10  return Promise.resolve("Hello, world!");
11}

If an async function throws an error, the returned Promise rejects instead of resolving. This is the foundation that makes try/catch work inside async functions.

How Await Pauses Execution

The await keyword can only be used inside an async function (or at the top level of an ES module). When the runtime encounters await, it pauses the function's execution until the awaited Promise settles, then resumes with the resolved value.

javascript
1async function fetchUser(userId) {
2  const response = await fetch(`/api/users/${userId}`);
3  const user = await response.json();
4  console.log(user.name);
5  return user;
6}

Think of each await as a checkpoint. The function yields control back to the event loop at that point, allowing other code to run while the Promise is pending. Once the Promise resolves, execution picks up on the next line.

Error Handling With try/catch

Because await unwraps Promises, rejected Promises throw exceptions that you catch with standard try/catch blocks. This replaces the .catch() callback pattern.

javascript
1async function loadData() {
2  try {
3    const response = await fetch("/api/data");
4    if (!response.ok) {
5      throw new Error(`HTTP error: ${response.status}`);
6    }
7    const data = await response.json();
8    return data;
9  } catch (error) {
10    console.error("Failed to load data:", error.message);
11    return null;
12  }
13}

Without the try/catch, a rejected Promise would propagate as an unhandled rejection, which modern runtimes treat as an error. Always wrap await calls that might fail.

Sequential vs Parallel Await

A common mistake is awaiting independent Promises one after another when they could run at the same time. Sequential awaits add up the wait times, while Promise.all runs them in parallel.

javascript
1// Sequential: total time is roughly timeA + timeB
2async function sequential() {
3  const a = await fetchEndpointA(); // waits for A to finish
4  const b = await fetchEndpointB(); // then starts B
5  return [a, b];
6}
7
8// Parallel: total time is roughly max(timeA, timeB)
9async function parallel() {
10  const [a, b] = await Promise.all([
11    fetchEndpointA(),
12    fetchEndpointB(),
13  ]);
14  return [a, b];
15}

Use sequential awaits when one operation depends on the result of another. Use Promise.all when the operations are independent and you want to minimize total wait time.

Comparison With .then() Chains

The .then() approach and async/await both work with Promises, but they differ in readability. Here is the same logic written both ways.

javascript
1// .then() chain
2function getUserPosts(userId) {
3  return fetch(`/api/users/${userId}`)
4    .then((res) => res.json())
5    .then((user) => fetch(`/api/posts?author=${user.id}`))
6    .then((res) => res.json())
7    .catch((err) => console.error(err));
8}
9
10// async/await equivalent
11async function getUserPosts(userId) {
12  try {
13    const userRes = await fetch(`/api/users/${userId}`);
14    const user = await userRes.json();
15    const postsRes = await fetch(`/api/posts?author=${user.id}`);
16    return await postsRes.json();
17  } catch (err) {
18    console.error(err);
19  }
20}

The async/await version is easier to step through in a debugger and handles branching logic (if/else) more naturally than nested .then() callbacks.

Common Pitfalls

  • Forgetting await: Calling an async function without await gives you a Promise object instead of the resolved value, which leads to subtle bugs where comparisons and operations silently pass on the Promise rather than the data.
  • Using await in a loop when parallelism is possible: Placing await inside a for loop serializes every iteration. Use Promise.all with map when iterations are independent of each other.
  • Missing error handling: An unhandled rejected Promise inside an async function can crash your Node.js process or produce confusing console warnings in browsers. Always add try/catch or a .catch() on the caller side.
  • Blocking the event loop with synchronous work after await: await only yields while the Promise is pending. If you run CPU-heavy synchronous code after the await resumes, you still block the event loop during that stretch.
  • Returning await unnecessarily in a return statement: Writing return await somePromise() inside a try/catch is useful, but outside a try/catch the extra await adds no benefit. A plain return somePromise() forwards the Promise directly.

Summary

  • An async function always returns a Promise, wrapping whatever value you return.
  • await pauses execution of the enclosing async function until the Promise resolves or rejects.
  • Use try/catch for error handling instead of chaining .catch() callbacks.
  • Reach for Promise.all when you need to run independent asynchronous tasks in parallel rather than sequentially.
  • The async/await syntax and .then() chains accomplish the same goal, but async/await produces code that is easier to read, debug, and maintain.

Course illustration
Course illustration

All Rights Reserved.