async/await is syntactic sugar on top of Promises. It lets us write asynchronous code that looks and reads like synchronous code. Under the hood, it’s still Promises — just with a much cleaner syntax.
The basics
An async function always returns a Promise. The await keyword pauses execution inside the async function until the Promise resolves, and then gives us the resolved value.
async function getUser() {
const response = await fetch("/api/user"); // pauses here until fetch completes
const user = await response.json(); // pauses here until parsing completes
return user; // automatically wrapped in a Promise
}
// Calling it
getUser().then(user => console.log(user));
Without async/await, the same code with Promises would look like:
function getUser() {
return fetch("/api/user")
.then(response => response.json())
.then(user => user);
}
Both do the exact same thing. But the async/await version reads top-to-bottom like normal code.
Error Handling with try/catch
Instead of .catch(), we use try/catch — just like synchronous error handling.
async function getUser() {
try {
const response = await fetch("/api/user");
const user = await response.json();
console.log(user);
} catch (error) {
console.log("Failed to fetch user:", error);
} finally {
console.log("Done"); // always runs
}
}
This is much nicer than chaining .then().catch() everywhere, especially when we have multiple await calls.
Sequential vs Parallel Execution
This is a very important concept and a common interview question.
Sequential (slow) — one after another
async function loadData() {
const users = await fetchUsers(); // waits 2 seconds
const posts = await fetchPosts(); // waits 2 more seconds
const comments = await fetchComments(); // waits 2 more seconds
// Total: ~6 seconds (each one waits for the previous)
}
Parallel (fast) — all at once with Promise.all
If the requests don’t depend on each other, we should fire them all at once:
async function loadData() {
const [users, posts, comments] = await Promise.all([
fetchUsers(), // starts immediately
fetchPosts(), // starts immediately
fetchComments() // starts immediately
]);
// Total: ~2 seconds (all run in parallel, we wait for the slowest)
}
Common mistake: await in forEach
This is a trap that catches a lot of people. forEach does not wait for async callbacks. It fires them all off and moves on.
// WRONG — these all fire at once, forEach doesn't wait
const ids = [1, 2, 3];
ids.forEach(async (id) => {
const user = await fetchUser(id);
console.log(user); // order is NOT guaranteed
});
console.log("Done"); // this runs BEFORE any user is fetched!
If we need sequential processing, use a for...of loop:
// CORRECT — processes one at a time, in order
for (const id of ids) {
const user = await fetchUser(id);
console.log(user); // guaranteed order
}
console.log("Done"); // this runs AFTER all users are fetched
If we want parallel processing but still need to wait for all of them:
// CORRECT — parallel processing, wait for all
const users = await Promise.all(ids.map(id => fetchUser(id)));
console.log(users); // all users, in order
async/await with arrow functions
We can use async with arrow functions too:
const getUser = async (id) => {
const response = await fetch(`/api/users/${id}`);
return response.json();
};
Key things to remember
asyncfunctions always return a Promise, even if we return a plain valueawaitcan only be used inside anasyncfunction (or at the top level of a module)awaitonly pauses the currentasyncfunction, not the entire program- Under the hood, everything after
awaitgoes to the Microtask Queue (just like.then()) - For independent async operations, always use
Promise.all()for better performance
In simple language, async/await lets us write async code that looks like regular synchronous code. It’s still Promises under the hood, but with a cleaner syntax. Use try/catch for errors, Promise.all for parallel operations, and never use await inside forEach.