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
asyncfunctions and useawaitto pause on Promise resolution - Handle errors in async functions using
try/catch - Explain how
async/awaitrelates 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/catchto handle errors in async functions. It covers all failure points in thetryblock in one place, including multipleawaitcalls that might each fail.
⚠️ Neutral Note: Async functions always return a Promise. If you call an async function and do not
awaitthe result or chain.then()/.catch(), errors from inside it will be unhandled Promise rejections that are hard to track down.
❌ Negative Pattern: Do not
awaitinside aforEachloop expecting sequential execution.forEachdoes not understand Promises — each callback fires without waiting for the previous one. Use afor...ofloop withawaitinstead.
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
asyncbefore a function makes it return a Promise and allowsawaitinside itawaitpauses the async function until a Promise resolves and gives you the resolved value- Use
try/catchinside async functions to handle errors from anyawaitcall in the block async/awaitis built on Promises — anasyncfunction always returns a Promise, andawaitis equivalent to.then()