Intermediate40 minjavascriptasyncawaitpromises

async/await Patterns

Rewrite Promise chains as clean, readable async/await functions, handle errors with try/catch, and run multiple requests in parallel with Promise.all().

Learning Objectives

By the end of this lesson, you'll be able to:

  • βœ“ Explain Explain what `async` and `await` do and how they relate to Promises
  • βœ“ Rewrite a `.then()` chain as an `async`/`await` function
  • βœ“ Handle errors in async functions using `try`/`catch`
  • βœ“ Run multiple independent requests in parallel using `Promise.all()`
  • βœ“ Avoid the most common async/await mistakes

Why This Matters:

Rewrite Promise chains as clean, readable async/await functions, handle errors with try/catch, and run multiple requests in parallel with Promise.all().

Before You Start:

You should be familiar with:

The async keyword

Adding async before a function declaration does two things:

  1. It allows you to use await inside that function
  2. It makes the function always return a Promise
async function greet() {
  return 'Hello';
}

greet().then((message) => console.log(message)); // 'Hello'

Even though greet just returns a plain string, async wraps it in a resolved Promise automatically. This means async functions always fit into Promise chains β€” you can call .then() on them.


The await keyword

await pauses execution within the async function until a Promise resolves, then gives you the resolved value directly β€” no .then() needed.

async function loadUser() {
  const response = await fetch('https://jsonplaceholder.typicode.com/users/1');
  const user = await response.json();
  console.log(user.name);
}

Compare this to the .then() version:

function loadUser() {
  fetch('https://jsonplaceholder.typicode.com/users/1')
    .then((response) => response.json())
    .then((user) => console.log(user.name));
}

Same result. The async/await version reads like a simple sequence of steps β€” which is exactly what it is.

Important: await only pauses the function it's inside. The rest of your program keeps running. It doesn't freeze the browser.


Promise chain

fetch(...)
  .then(...)
  .then(...)
  .catch(...)

async/await

try {
  const data = await ...
} catch (error) {
  ...
}
async/await keeps the order of operations readable without changing the underlying Promise-based engine.

Error handling with try/catch

With .then() chains, you use .catch() for errors. With async/await, you use a standard try/catch block:

async function loadUser(id) {
  try {
    const response = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`);

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

    const user = await response.json();
    console.log(user.name);

  } catch (error) {
    console.error('Failed to load user:', error.message);
  }
}

The catch block handles everything that could go wrong inside the try β€” network failures, the response.ok check, parsing errors, anything. One handler for all of it.


Rewriting a Promise chain

Here's a direct before/after comparison β€” the same logic in both styles.

Promise chain:

function loadPostTitles(userId) {
  return fetch(`https://jsonplaceholder.typicode.com/posts?userId=${userId}`)
    .then((response) => {
      if (!response.ok) throw new Error(`HTTP error: ${response.status}`);
      return response.json();
    })
    .then((posts) => posts.map((post) => post.title))
    .catch((error) => {
      console.error(error.message);
      return [];
    });
}

async/await equivalent:

async function loadPostTitles(userId) {
  try {
    const response = await fetch(`https://jsonplaceholder.typicode.com/posts?userId=${userId}`);

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

    const posts = await response.json();
    return posts.map((post) => post.title);

  } catch (error) {
    console.error(error.message);
    return [];
  }
}

Both functions return a Promise that resolves to an array of titles (or an empty array on error). The async/await version is easier to follow, especially once the logic grows.


Running requests in parallel

One common mistake with async/await is making requests sequential when they could run at the same time.

Slow β€” sequential (each waits for the previous):

async function loadDashboard() {
  const user  = await getUser(1);    // wait...
  const posts = await getPosts(1);   // then wait...
  const todos = await getTodos(1);   // then wait...
  // Total time: sum of all three requests
}

If getUser, getPosts, and getTodos don't depend on each other, there's no reason to queue them. Promise.all() runs them in parallel and waits for all to finish:

Fast β€” parallel:

async function loadDashboard() {
  const [user, posts, todos] = await Promise.all([
    getUser(1),
    getPosts(1),
    getTodos(1),
  ]);
  // Total time: the slowest single request
}

Promise.all() takes an array of Promises and returns a single Promise that resolves when all of them have resolved β€” giving you an array of results in the same order. If any one rejects, the whole thing rejects.

Here's a real example with fetch:

async function loadUserWithPosts(userId) {
  try {
    const [userRes, postsRes] = await Promise.all([
      fetch(`https://jsonplaceholder.typicode.com/users/${userId}`),
      fetch(`https://jsonplaceholder.typicode.com/posts?userId=${userId}`),
    ]);

    if (!userRes.ok || !postsRes.ok) {
      throw new Error('One or more requests failed');
    }

    const [user, posts] = await Promise.all([
      userRes.json(),
      postsRes.json(),
    ]);

    return { user, posts };

  } catch (error) {
    console.error(error.message);
    return null;
  }
}

Sequential requests compared with parallel Promise.all requests.Sequentialuser requestposts requesttodos requestParallel with Promise.alluser requestposts requesttodos requestwait once
Independent requests can run side by side; Promise.all waits once at the end instead of forcing each request to queue behind the previous one.

Common mistakes

Forgetting await

Without await, you get the Promise object β€” not the resolved value:

async function broken() {
  const response = fetch('https://jsonplaceholder.typicode.com/users/1'); // missing await
  console.log(response); // Promise { <pending> } β€” not the response
}

If your data looks like Promise { <pending> }, a missing await is almost always the cause.

Using await outside an async function

await is only valid inside an async function. Using it at the top level of a regular script will throw a syntax error (though modern environments support top-level await in ES modules).

// This will throw in most contexts
const user = await getUser(1); // SyntaxError

Wrap it in an async function, or use an immediately invoked async function expression (IIFE):

(async () => {
  const user = await getUser(1);
  console.log(user.name);
})();

Making sequential requests when parallel is possible

If two requests don't depend on each other's results, use Promise.all(). Sequential await calls for independent data is one of the most common performance issues in async JavaScript.


⏸️ Check Your Understanding

Before moving forward, can you answer these?

  1. 1. What does adding `async` to a function do?
  2. 2. What does `await` actually do?
  3. 3. How does error handling differ between `.then()` chains and `async`/`await`?
  4. 4. When should you use `Promise.all()` instead of sequential `await` calls?
Check Your Answers
  1. It allows the use of `await` inside the function, and makes the function always return a Promise β€” even if it returns a plain value.
  2. It pauses execution within the async function until the awaited Promise resolves, then gives you the resolved value directly. It doesn't block the rest of the program.
  3. With `.then()` chains you add `.catch()` at the end. With `async`/`await` you use a `try`/`catch` block inside the function. Both catch the same errors.
  4. When the requests are independent β€” when one doesn't need the result of another to run. `Promise.all()` runs them in parallel, reducing total wait time to the slowest single request.

How confident are you with this concept?

πŸ˜• Still confused | πŸ€” Getting there | 😊 Got it! | πŸŽ‰ Could explain it to a friend!

Guided Practice

<!-- GuidedPractice component -->

Step 1 β€” Start with a Promise chain

Copy this function into your editor:

function getUserAndPosts(userId) {
  return fetch(`https://jsonplaceholder.typicode.com/users/${userId}`)
    .then((response) => {
      if (!response.ok) throw new Error(`HTTP error: ${response.status}`);
      return response.json();
    })
    .then((user) => {
      return fetch(`https://jsonplaceholder.typicode.com/posts?userId=${user.id}`)
        .then((response) => response.json())
        .then((posts) => ({ user, posts }));
    })
    .catch((error) => {
      console.error(error.message);
      return null;
    });
}

Step 2 β€” Rewrite it as async/await

Create a new function getUserAndPostsAsync(userId) that produces the same result using async/await and try/catch.

Hint β€” structure

async function getUserAndPostsAsync(userId) {
  try {
    const userRes = await fetch(...);
    if (!userRes.ok) throw new Error(...);
    const user = await userRes.json();

    const postsRes = await fetch(...);
    const posts = await postsRes.json();

    return { user, posts };
  } catch (error) {
    console.error(error.message);
    return null;
  }
}

Step 3 β€” Call it and log the results

Call getUserAndPostsAsync(1) and log the user's name and the number of posts returned.

Step 4 β€” Optimise with Promise.all()

Notice that in your rewrite, the posts request waits for the user request to finish β€” but it doesn't need to, because you're passing the userId directly. Refactor to fetch both in parallel using Promise.all().

πŸ’ͺ Independent Practice

<!-- IndependentPractice component -->

Your Task:

Build an async data loader that fetches three independent resources in parallel and combines the results.

Requirements:
  • Write an `async` function `loadSummary()` that uses `Promise.all()` to fetch simultaneously:
  • A single user: `https://jsonplaceholder.typicode.com/users/3`
  • Their posts: `https://jsonplaceholder.typicode.com/posts?userId=3`
  • Their todos: `https://jsonplaceholder.typicode.com/todos?userId=3`
  • Use `try`/`catch` for error handling
  • Check `response.ok` for each response and throw a descriptive error if any fail
  • Return an object in this shape:
  • Call `loadSummary()` and log the result in a readable format

Success Criteria:

CriteriaYou've succeeded if...
All three fetches run in parallel via `Promise.all()`Completed clearly and correctly in your solution.
All three responses are checked with `response.ok`Completed clearly and correctly in your solution.
`try`/`catch` handles errors without crashingCompleted clearly and correctly in your solution.
The returned object has exactly the four properties listedCompleted clearly and correctly in your solution.
The function is called and the result is logged clearlyCompleted clearly and correctly in your solution.

What's next

Key Takeaways:

  • Explain what `async` and `await` do and how they relate to Promises
  • Rewrite a `.then()` chain as an `async`/`await` function
  • Handle errors in async functions using `try`/`catch`
  • Run multiple independent requests in parallel using `Promise.all()`
  • Avoid the most common async/await mistakes

Learning Objectives Review:

Look back at what you set out to learn. Can you now:

  • βœ… Explain what `async` and `await` do and how they relate to Promises Check!
  • βœ… Rewrite a `.then()` chain as an `async`/`await` function Got it!
  • βœ… Handle errors in async functions using `try`/`catch` Can explain it!
  • βœ… Run multiple independent requests in parallel using `Promise.all()` Could teach this!
  • βœ… Avoid the most common async/await mistakes Check!

If you can confidently answer "yes" to most of these, you're ready to move on!

Think & Reflect:

πŸ’­ Pause and reflect

  • Which idea from this lesson now feels practical rather than abstract?
  • What would you build or test next to make this stick?

🎯 Looking Ahead:


You now have the full async toolkit: Promises, fetch(), JSON, and async/await. The final tutorial in this section puts it all together in a practical mini-project β€” fetching real data from a public API and rendering it to the page.

β†’ Next: Mini-project: Fetch and Display Live Data

Recommended Next Steps

Continue Learning

Ready to move forward? Continue with the next tutorial in this series:

Mini-project: Fetch and Display Live Data

Related Topics

Explore these related tutorials to expand your knowledge:

Progress tracking is disabled. Enable it in to track your completed tutorials.