JavaScript Async/Await

Intermediate 8 min read

async/await makes asynchronous JavaScript code look and behave like synchronous code. It's built on top of Promises and provides a cleaner, more readable way to handle async operations.

What is Async/Await?

Async/await is syntactic sugar over Promises. The async keyword declares a function as asynchronous, and await pauses execution until a Promise resolves.

javascript
// Declare an async function
async function fetchData() {
  // await pauses until the promise resolves
  const response = await fetch('/api/data');
  const data = await response.json();
  return data;
}

// Call it (returns a Promise)
fetchData().then(data => console.log(data));
Note
An async function always returns a Promise. If you return a value, it's automatically wrapped in Promise.resolve().

Why Use Async/Await?

Before async/await, we used Promise chains with .then(). While Promises are powerful, chaining them can become hard to read. Compare these two approaches:

javascript
// With Promises (chaining)
fetch('/api/user')
  .then(response => response.json())
  .then(user => fetch(`/api/posts/${user.id}`))
  .then(response => response.json())
  .then(posts => console.log(posts))
  .catch(error => console.error(error));

// With async/await (cleaner)
async function getUserPosts() {
  const response = await fetch('/api/user');
  const user = await response.json();
  const postsResponse = await fetch(`/api/posts/${user.id}`);
  const posts = await postsResponse.json();
  console.log(posts);
}
Key Benefit
Async/await makes asynchronous code read like synchronous code, making it easier to understand, write, and debug.

Try It Yourself

Run this example to see async/await in action:

index.js
// Simulating an API call with a delay
function fetchUser(id) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({ id, name: "Alice", email: "[email protected]" });
    }, 1000);
  });
}

// Using async/await
async function getUser() {
  console.log("Fetching user...");
  const user = await fetchUser(1);
  console.log("User fetched:", user);
  return user;
}

getUser();

Callbacks vs Promises vs Async/Await

FeatureCallbacksPromisesAsync/Await
ReadabilityPoorGoodExcellent
Error HandlingDifficult.catch()try/catch
ChainingCallback Hell.then() chainsSequential code
DebuggingHardModerateEasy
Return Values

Error Handling

Use try/catch blocks to handle errors in async functions. This works just like synchronous error handling.

javascript
async function fetchUser() {
  try {
    const response = await fetch('/api/user');

    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }

    const user = await response.json();
    return user;
  } catch (error) {
    console.error('Failed to fetch user:', error.message);
    throw error; // Re-throw if you want calling code to handle it
  }
}

Try this interactive example that demonstrates error handling:

index.js
// Simulating an API that might fail
function fetchData(shouldFail) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (shouldFail) {
        reject(new Error("Network error: Failed to fetch"));
      } else {
        resolve({ data: "Success!" });
      }
    }, 500);
  });
}

// Handling errors with try/catch
async function getData() {
  try {
    console.log("Attempting to fetch data...");
    const result = await fetchData(true); // This will fail
    console.log("Result:", result);
  } catch (error) {
    console.error("Caught error:", error.message);
  } finally {
    console.log("Cleanup: fetch attempt complete");
  }
}

getData();
Don't Forget Error Handling
Unhandled Promise rejections can crash your application in Node.js. Always wrap await calls in try/catch or add a .catch() when calling async functions.

Parallel Execution

Multiple await statements run sequentially. To run operations in parallel, use Promise.all():

javascript
// Run multiple requests in parallel
async function fetchDashboard() {
  const [user, posts, notifications] = await Promise.all([
    fetch('/api/user').then(r => r.json()),
    fetch('/api/posts').then(r => r.json()),
    fetch('/api/notifications').then(r => r.json())
  ]);

  return { user, posts, notifications };
}

Promise.allSettled

Unlike Promise.all() which rejects if any promise rejects, Promise.allSettled() waits for all promises and tells you which succeeded or failed:

javascript
// Get results even if some fail
async function fetchAllData() {
  const results = await Promise.allSettled([
    fetch('/api/users'),
    fetch('/api/posts'),
    fetch('/api/will-fail')
  ]);

  results.forEach((result, i) => {
    if (result.status === 'fulfilled') {
      console.log(`Request ${i} succeeded`);
    } else {
      console.log(`Request ${i} failed: ${result.reason}`);
    }
  });
}

See the performance difference between sequential and parallel execution:

index.js
// Simulating multiple API calls
function fetchUser() {
  return new Promise(resolve => {
    setTimeout(() => resolve({ name: "Alice" }), 1000);
  });
}

function fetchPosts() {
  return new Promise(resolve => {
    setTimeout(() => resolve([{ title: "Post 1" }, { title: "Post 2" }]), 800);
  });
}

function fetchComments() {
  return new Promise(resolve => {
    setTimeout(() => resolve([{ text: "Great!" }]), 600);
  });
}

// Sequential (slow) - takes ~2.4 seconds
async function fetchSequential() {
  console.time("Sequential");
  const user = await fetchUser();
  const posts = await fetchPosts();
  const comments = await fetchComments();
  console.timeEnd("Sequential");
  console.log("Sequential result:", { user, posts, comments });
}

// Parallel (fast) - takes ~1 second
async function fetchParallel() {
  console.time("Parallel");
  const [user, posts, comments] = await Promise.all([
    fetchUser(),
    fetchPosts(),
    fetchComments()
  ]);
  console.timeEnd("Parallel");
  console.log("Parallel result:", { user, posts, comments });
}

// Run both to compare
fetchSequential();
setTimeout(fetchParallel, 3000);

Async/Await in Loops

Be careful when using async/await in loops. The behavior differs between for...of and array methods like forEach.

javascript
// Sequential - each iteration waits for previous
async function processSequential(items) {
  for (const item of items) {
    await processItem(item); // Waits before next iteration
  }
}

// Parallel - all items processed at once
async function processParallel(items) {
  await Promise.all(items.map(item => processItem(item)));
}

Common Mistakes

1. Using forEach with async/await

forEach Doesn't Wait
forEach doesn't understand async functions. The iterations don't wait for each other, and the code after forEach runs before the async operations complete.
javascript
// WRONG: forEach doesn't wait for async
async function wrong() {
  const items = [1, 2, 3];
  items.forEach(async (item) => {
    await processItem(item); // These run in parallel, not sequentially!
  });
  console.log('Done'); // This runs before items are processed!
}

// CORRECT: Use for...of for sequential processing
async function correct() {
  const items = [1, 2, 3];
  for (const item of items) {
    await processItem(item);
  }
  console.log('Done'); // This runs after all items are processed
}

2. Forgetting to await

Warning
If you forget await, you get a Promise object instead of the resolved value. This is a common source of bugs.

3. Sequential when parallel is possible

Tip
If operations don't depend on each other, use Promise.all() to run them in parallel. This can dramatically improve performance.

Best Practices

1. Always Handle Errors
Wrap await calls in try/catch blocks. Unhandled rejections can crash your app.
2. Use Promise.all for Parallel Operations
When fetching multiple independent resources, use Promise.all() instead of sequential awaits.
3. Avoid Mixing Patterns
Stick to async/await throughout your codebase. Mixing .then() chains with await makes code harder to follow.
4. Use for...of for Sequential Loops
Use for...of loops when you need sequential async iteration. Never use forEach with async functions.

Test Your Knowledge

Test Your Knowledge

5 questions
Question 1

What does the async keyword do to a function?

Question 2

What happens when you use await outside an async function?

Question 3

How do you run multiple async operations in parallel?

Question 4

What is the best way to handle errors in async/await?

Question 5

What does Promise.all() return if one promise rejects?

Practice Exercises

Challenge 1: Convert Promise Chain

Easy

Convert the promise chain to use async/await syntax. The function should fetch a user and then fetch their posts.

Starter Code
// Challenge: Convert this promise chain to async/await
// The function should fetch a user, then fetch their posts

function getUserWithPosts(userId) {
  return fetch(`/api/users/${userId}`)
    .then(response => response.json())
    .then(user => {
      return fetch(`/api/users/${userId}/posts`)
        .then(response => response.json())
        .then(posts => {
          return { user, posts };
        });
    });
}

// Your async/await version here:
async function getUserWithPostsAsync(userId) {
  // TODO: Convert the above to async/await
}

Challenge 2: Parallel Fetch with Error Handling

Medium

Fetch multiple URLs in parallel, but return null for any requests that fail instead of throwing an error.

Starter Code
// Challenge: Fetch 3 URLs in parallel and handle errors
// Return an array of results, with null for any that failed

const urls = [
  'https://api.example.com/users',
  'https://api.example.com/posts',
  'https://api.example.com/invalid' // This one might fail
];

async function fetchAllSafely(urls) {
  // TODO: Fetch all URLs in parallel
  // Return results array where failed requests return null
  // Hint: Use Promise.allSettled or try/catch with Promise.all
}

Frequently Asked Questions

Can I use await at the top level of a file?

Yes, but only in ES modules (files with .mjs extension or "type": "module" in package.json). This is called "top-level await" and was added in ES2022.

What's the difference between Promise.all and Promise.allSettled?

Promise.all() rejects immediately if any promise rejects. Promise.allSettled() waits for all promises and returns an array of results, telling you which succeeded and which failed.

Is async/await slower than Promises?

No, async/await compiles down to Promises. The performance is identical. The only difference is readability and developer experience.

How do I cancel an async operation?

Use AbortController. Create a controller, pass its signal to fetch or other APIs, and call controller.abort() to cancel. The promise will reject with an AbortError.

Summary

You've learned how to use async/await to write cleaner asynchronous JavaScript:

  • async - Declares a function that returns a Promise
  • await - Pauses execution until a Promise resolves
  • try/catch - Handles errors in async functions
  • Promise.all() - Runs multiple operations in parallel

Async/await makes asynchronous code much easier to read and maintain. Use it whenever you're working with Promises, API calls, or any asynchronous operations.