Overview
async/await is the modern way to handle asynchronous operations in JavaScript. It builds on Promises but makes asynchronous code read like sequential code. This tutorial covers the syntax, error handling, and the concurrency patterns that matter in production.
From Callbacks to Promises to async/await
| Era | Style | Problem |
|---|---|---|
| Callbacks | fs.readFile(path, cb) | Callback hell with nested error handling |
| Promises | fetch(url).then(...) | Still chains; error handling scattered |
| async/await | const res = await fetch(url) | Reads like synchronous code |
Basic Syntax
async function getUser(id) {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
}
getUser(1).then(user => console.log(user));
An async function always returns a Promise. await pauses execution until the Promise settles, then returns the resolved value.
Error Handling
async function loadProfile(id) {
try {
const user = await getUser(id);
const posts = await getPosts(user.id);
return { user, posts };
} catch (err) {
console.error("Failed to load profile:", err.message);
throw err;
} finally {
console.log("Request finished");
}
}
try/catch/finally works with await exactly as it does with synchronous code.
Sequential vs Parallel Execution
// Sequential — total time = t1 + t2 + t3
const user = await getUser(1);
const posts = await getPosts(1);
const comments = await getComments(1);
// Parallel — total time = max(t1, t2, t3)
const [user, posts, comments] = await Promise.all([
getUser(1),
getPosts(1),
getComments(1)
]);
Use Promise.all whenever the requests do not depend on each other.
Promise Combinators
| Method | Behavior |
|---|---|
Promise.all | Waits for all; rejects on first failure |
Promise.allSettled | Waits for all; returns status for each |
Promise.race | Settles with the first completed promise |
Promise.any | Resolves with the first fulfilled promise |
Using async/await in Loops
// Sequential — correct when order matters
for (const id of ids) {
const user = await getUser(id);
results.push(user);
}
// Parallel with concurrency limit
async function mapLimit(items, limit, fn) {
const results = [];
const executing = [];
for (const item of items) {
const p = Promise.resolve().then(() => fn(item));
results.push(p);
if (executing.length >= limit) {
await Promise.race(executing);
}
executing.push(p);
}
return Promise.all(results);
}
Top-Level await
In ES modules, await can be used at the top level without wrapping in an async function:
// module.mjs
const config = await fetch("/config.json").then(r => r.json());
export default config;
Common Pitfalls
| Pitfall | Problem | Fix |
|---|---|---|
forEach with async callback | Does not await; runs in parallel silently | Use for...of or Promise.all(items.map(...)) |
Missing await | Function returns a Promise instead of the value | Add await |
| Awaiting inside a loop for independent calls | Serializes what could be parallel | Collect promises, then await Promise.all |
| Unhandled rejections | Crashes in Node.js | Wrap in try/catch or attach a global handler |
