Async and Await

Course: JavaScript Intermediate

Lesson 16: Async and Await

Level

Intermediate

Estimated Reading Time

10–15 minutes

Goal

By the end of this lesson, the learner will be able to:

  • Write async functions and use await to pause on Promise resolution
  • Handle errors in async functions using try/catch
  • Explain how async/await relates to Promises under the hood

Frontend Development Context

async/await is the modern syntax for writing asynchronous JavaScript code in a way that reads like synchronous code. Instead of chaining .then() and .catch() calls, you write step-by-step logic that pauses at each await and resumes when the Promise resolves. This dramatically reduces the visual complexity of multi-step async operations.

In real frontend projects, async/await is how most developers write code that fetches data, saves to a server, or sequences multiple API calls. React components use it in event handlers and useEffect. Understanding it deeply — including what happens with errors and what async returns — makes your code more predictable and easier to maintain.

Explanation

The async keyword before a function declaration or expression marks it as an async function. Async functions always return a Promise. If you return a plain value like "hello", it is automatically wrapped in a resolved Promise.

The await keyword can only be used inside an async function. It pauses execution of the async function until the Promise it is awaiting resolves, then gives you the resolved value. Other code outside the async function continues running while the async function is paused — this is not blocking.

If an awaited Promise rejects, it throws the rejection reason as an error inside the async function. You catch it with a standard try/catch block. This is more readable than a .catch() callback, especially when you have multiple await calls that might each fail.

You can await any Promise, including fetch, response.json(), custom Promises, and any library function that returns a Promise. The await simply waits for the Promise to settle and extracts its resolved value.

async/await is syntactic sugar over Promises — it does not replace them. An async function returns a Promise, and await calls .then() internally. Understanding Promises first (Lesson 15) makes async/await easier to reason about when something goes wrong.

Code Example

This example uses async/await with try/catch to fetch a list of posts and render them, including handling errors and a loading state.

async function loadUserPosts(userId) {
  const statusEl = document.querySelector("#status");
  const listEl = document.querySelector("#postList");

  statusEl.textContent = "Loading posts...";

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

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

    const posts = await response.json();

    listEl.innerHTML = "";
    posts.slice(0, 3).forEach(post => {
      const li = document.createElement("li");
      li.textContent = post.title;
      listEl.appendChild(li);
    });

    statusEl.textContent = `Loaded ${posts.length} posts`;
  } catch (error) {
    statusEl.textContent = "Failed to load posts.";
    console.error("loadUserPosts error:", error.message);
  }
}

loadUserPosts(1);

Code Explanation

async function loadUserPosts(userId) declares an async function. Everything inside it can use await. The function returns a Promise, though in this case we call it as a fire-and-forget (we do not chain .then() on the call).

await fetch(...) pauses the function until the fetch Promise resolves, then stores the response object in response. Without await, response would be a Promise object — not the response itself.

if (!response.ok) throw new Error(...) manually converts an HTTP error into a thrown exception. Because we are inside a try block, this immediately jumps to the catch block.

const posts = await response.json() awaits the second async operation — reading and parsing the response body. After this line, posts is a plain JavaScript array of post objects.

The catch (error) block handles any error thrown in the try block — whether from a network failure, the manual throw, or a JSON parsing error. The UI is updated to show a failure message and the error is logged to the console.

Pattern Highlights

Positive Pattern: Use try/catch to handle errors in async functions. It covers all failure points in the try block in one place, including multiple await calls that might each fail.

⚠️ Neutral Note: Async functions always return a Promise. If you call an async function and do not await the result or chain .then()/.catch(), errors from inside it will be unhandled Promise rejections that are hard to track down.

Negative Pattern: Do not await inside a forEach loop expecting sequential execution. forEach does not understand Promises — each callback fires without waiting for the previous one. Use a for...of loop with await instead.

Common Mistakes

Mistake 1: Using await outside an async function

Trying to use await at the top level of a regular script (without module context).

const data = await fetch("/api/data"); // SyntaxError: await is only valid in async functions

await can only be used inside an async function (or at the top level of an ES module). Wrap your code in an async function or use .then() at the top level.

Mistake 2: Not awaiting the async function's result when you need it

Calling an async function and immediately using its result as if it were synchronous.

async function getName() { return "Alex"; }

const name = getName(); // name is a Promise, not "Alex"
console.log(name);     // Promise { 'Alex' }

To get the resolved value, either await the call inside another async function: const name = await getName(), or chain .then(): getName().then(n => console.log(n)).

Mistake 3: Awaiting inside forEach expecting sequential behavior

Using await inside a forEach callback and expecting each iteration to wait.

const ids = [1, 2, 3];
ids.forEach(async (id) => {
  const data = await fetchLesson(id);
  console.log(data); // all three run in parallel, not sequentially
});

forEach fires all callbacks immediately without waiting. Use for...of with await for sequential execution, or Promise.all with map for parallel execution.

Quick Recap

  • async before a function makes it return a Promise and allows await inside it
  • await pauses the async function until a Promise resolves and gives you the resolved value
  • Use try/catch inside async functions to handle errors from any await call in the block
  • async/await is built on Promises — an async function always returns a Promise, and await is equivalent to .then()

Ready to test your understanding? Take the quiz to reinforce what you learned.

© 2026 Ant Skillsv.26.07.07-23:01