Async/Await
Async/await is syntax for working with Promises that makes asynchronous code look and behave like synchronous code. An async function always returns a Promise; await pauses execution until the awaited Promise settles.
Explanation
Async/await is syntactic sugar — it compiles to Promises under the hood. An async function returns a Promise. Inside an async function, await pauses execution at that point until the awaited Promise resolves, then resumes with the resolved value. Unhandled rejections propagate as thrown exceptions, which try/catch can handle. The result is Promise-based async code that reads top-to-bottom like synchronous code. Error handling: without try/catch, any rejected Promise inside an async function causes the function's returned Promise to reject. With try/catch, you handle errors locally. You can mix both: let some errors propagate (by omitting try/catch) and handle others locally (by wrapping specific awaits). The most common async/await mistake is sequential awaiting when parallel execution is possible. await user = fetchUser(); await posts = fetchPosts(user.id); — these await statements run one after the other. If the operations are independent (don't depend on each other's results), use Promise.all: const [users, settings] = await Promise.all([fetchUsers(), fetchSettings()]); — both run concurrently. Top-level await (stable in ES2022 and Node.js 14+, enabled by default in ES modules) allows await at the top level of a module without wrapping in an async function. This simplifies module initialization code that needs to fetch configuration or connect to a database before exporting.
Code Example
javascript// Async/await fundamentals
async function fetchUserWithPosts(userId) {
try {
// Sequential: post fetch waits for user fetch to complete
const user = await fetch(`/api/users/${userId}`).then(r => r.json());
const posts = await fetch(`/api/posts?userId=${user.id}`).then(r => r.json());
return { user, posts };
} catch (err) {
console.error('Failed to fetch:', err);
throw err; // re-throw if callers need to handle it
}
}
// Parallel: both requests fire simultaneously
async function fetchDashboard() {
const [user, settings, notifications] = await Promise.all([
fetch('/api/user').then(r => r.json()),
fetch('/api/settings').then(r => r.json()),
fetch('/api/notifications').then(r => r.json()),
]);
return { user, settings, notifications };
}
// 3x faster than sequential awaits for independent requests
// async IIFE for top-level await in older environments
(async () => {
const data = await loadData();
console.log(data);
})();
// Handling individual errors without stopping the whole flow
async function safeLoad() {
const user = await fetchUser();
const settings = await fetchSettings().catch(() => defaultSettings);
// fetchSettings failure returns defaultSettings, not throw
return { user, settings };
}
Why It Matters for Engineers
Async/await is the dominant pattern for asynchronous JavaScript in modern codebases. React's data fetching (useEffect with async functions, React Query, SWR), Express route handlers, Node.js file and database operations — all use async/await. Writing it correctly (handling errors, knowing when to parallelize with Promise.all) is an everyday production engineering skill. The sequential vs parallel await distinction is a real performance concern: sequentially awaiting three 100ms API calls takes 300ms; Promise.all takes 100ms. For user-facing features, this difference is the difference between a snappy and a sluggish UI.
Related Terms
Learn This In Practice
Go deeper with the full module on Beyond Vibe Code.
Programming Foundations → →