Async/await is syntactic sugar over Promises that lets asynchronous code read like ordinary synchronous code. This lesson covers the async and await keywords, try/catch error handling, the difference between serial and parallel execution, the four Promise combinators, and top-level await.
1JavaScript Async/Await Part 1
Async/Await is syntactic sugar built on Promises. Instead of chaining .then() callbacks, you write flat, linear code that LOOKS synchronous — but runs asynchronously without blocking the browser.
// ❌ Promise chain — nested, hard to follow
fetch('/api/user')
.then(res => res.json())
.then(user => {
return fetch(`/api/posts/${user.id}`);
})
.then(res => res.json())
.then(posts => console.log(posts))
.catch(err => console.error(err));
// ✅ Async/Await — flat, linear, readable
async function loadPosts() {
const res = await fetch('/api/user');
const user = await res.json();
const postsRes = await fetch(`/api/posts/${user.id}`);
const posts = await postsRes.json();
console.log(posts);
}Flat is Better
.then()
.then()
await
await
2JavaScript Async/Await Part 2
The 'async' keyword marks a function as asynchronous. The critical rule: an async function ALWAYS returns a Promise — even if you return a plain value like a string or number.
// async function ALWAYS returns a Promise
async function getStatus() {
return 'Online'; // ← returns Promise<'Online'>
}
// Proof: .then() works on the result
getStatus().then(val => {
console.log(val); // 'Online'
console.log(typeof val); // 'string'
});
// Equivalent without async:
function getStatusManual() {
return Promise.resolve('Online');
}Always a Promise
⬇️
Promise.resolve('Online')
3JavaScript Async/Await Part 3
The 'await' keyword pauses execution of the async function until the Promise resolves. It UNWRAPS the Promise, giving you the resolved value directly — no .then() needed. The rest of your app keeps running while this function waits.
async function showUser() {
console.log('1. Start'); // runs immediately
const user = await fetchUser(); // ⏸ PAUSES here
// ↑ fetchUser() returns a Promise
// ↑ await unwraps it → user = resolved value
console.log('2. User:', user); // runs AFTER resolve
console.log('3. Done'); // runs AFTER line above
}
showUser();
console.log('4. Non-blocking!'); // runs IMMEDIATELYTimeline
4JavaScript Async/Await Part 4
Error handling with async/await uses the same try...catch blocks you already know from synchronous code. If ANY awaited Promise rejects, execution jumps to the catch block — network errors, API errors, JSON parse errors, all caught in one place.
async function loadUser(id) {
try {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) {
throw new Error(`HTTP ${res.status}`);
}
const user = await res.json();
console.log('User:', user.name);
return user;
} catch (error) {
console.error('Failed to load user:', error.message);
// Network error, HTTP error, or JSON parse error
// ALL caught here
} finally {
console.log('Request completed');
}
}Unified Catch
5JavaScript Async/Await Part 5
DANGER: awaiting one after another is SERIAL — each waits for the previous to finish. If tasks are independent, this wastes time. Two 2-second requests take 4 seconds serial, but only 2 seconds parallel.
// ❌ SERIAL — total time: ~4 seconds
async function loadSerial() {
const users = await fetchUsers(); // 2s ⏳
const posts = await fetchPosts(); // 2s ⏳ (waits for users!)
// Total: 2s + 2s = 4s
return { users, posts };
}
// Timeline:
// |--users (2s)--|--posts (2s)--|
// 0s 2s 4sSerial Bottleneck
6JavaScript Async/Await Part 6
Promise.all() fires all Promises at once and waits for ALL to resolve. Independent tasks run in parallel — two 2-second requests complete in just 2 seconds total. But if ANY one fails, the entire Promise.all rejects.
// ✅ PARALLEL — total time: ~2 seconds
async function loadParallel() {
const [users, posts] = await Promise.all([
fetchUsers(), // 2s ⏳ starts immediately
fetchPosts(), // 2s ⏳ starts immediately
]);
// Total: max(2s, 2s) = 2s ← 50% faster!
return { users, posts };
}
// Timeline:
// |--users (2s)--|
// |--posts (2s)--|
// 0s 2s ← both done!Parallel Execution
7JavaScript Async/Await Part 7
Beyond Promise.all: allSettled() waits for ALL regardless of failures. race() resolves/rejects with the FIRST to finish. any() resolves with the FIRST success (ignores rejections). Each solves a different concurrency pattern.
const promises = [
fetch('/api/fast'), // resolves in 1s
fetch('/api/slow'), // resolves in 3s
fetch('/api/broken'), // rejects in 2s
];
// Promise.all — FAIL-FAST (one reject = all reject)
await Promise.all(promises); // ❌ rejects at 2s
// Promise.allSettled — WAIT FOR ALL
await Promise.allSettled(promises);
// [{status:'fulfilled'}, {status:'fulfilled'}, {status:'rejected'}]
// Promise.race — FIRST TO FINISH (success or failure)
await Promise.race(promises); // resolves at 1s (fast wins)
// Promise.any — FIRST SUCCESS (ignores rejections)
await Promise.any(promises); // resolves at 1s (fast wins)Promise Combinators
All or nothing. Fail-fast.
Wait for all. Never rejects.
First to finish wins (success/fail).
First to SUCCEED wins.
8JavaScript Async/Await Part 8
Real-world pattern: manage loading, data, and error states with async/await. This is the exact pattern used in React, Vue, and every modern framework for data fetching.
async function loadDashboard() {
let loading = true;
let data = null;
let error = null;
try {
const [users, stats] = await Promise.all([
fetchUsers(),
fetchStats(),
]);
data = { users, stats };
} catch (err) {
error = err.message;
} finally {
loading = false;
}
// State: { loading: false, data: {...}, error: null }
return { loading, data, error };
}State Management
9JavaScript Async/Await Part 9
Modern JavaScript (ES modules) supports top-level await — you can use await outside any function at the module's root. No async wrapper needed. This is the standard in Node.js ESM and modern browsers.
// ── In an ES Module (.mjs or type="module") ──
// ✅ Top-level await — no async wrapper needed
const config = await fetch('/api/config').then(r => r.json());
console.log('App config loaded:', config.appName);
// ✅ Dynamic imports with await
const { default: Chart } = await import('./chart.js');
const chart = new Chart('#canvas');Top-Level Await
10JavaScript Async/Await Part 10
Async/Await mastered: async wraps returns in Promises, await pauses and unwraps, try/catch handles all errors, Promise.all for parallel execution, and allSettled/race/any for advanced patterns. Next: the Fetch API.
Async Mastered
11Step-by-Step Breakdown
Async/Await is syntactic sugar built on Promises. Instead of chaining .then() callbacks, you write flat, linear code that LOOKS synchronous — but runs asynchronously without blocking the browser.
The 'async' keyword marks a function as asynchronous. The critical rule: an async function ALWAYS returns a Promise — even if you return a plain value like a string or number.
The 'await' keyword pauses execution of the async function until the Promise resolves. It UNWRAPS the Promise, giving you the resolved value directly — no .then() needed. The rest of your app keeps running while this function waits.
Checkpoint: Can you use the 'await' keyword inside a regular function that is NOT marked with 'async'?
- →Yes, it works everywhere
- →No, 'await' requires an 'async' function
Error handling with async/await uses the same try...catch blocks you already know from synchronous code. If ANY awaited Promise rejects, execution jumps to the catch block — network errors, API errors, JSON parse errors, all caught in one place.
DANGER: awaiting one after another is SERIAL — each waits for the previous to finish. If tasks are independent, this wastes time. Two 2-second requests take 4 seconds serial, but only 2 seconds parallel.
Promise.all() fires all Promises at once and waits for ALL to resolve. Independent tasks run in parallel — two 2-second requests complete in just 2 seconds total. But if ANY one fails, the entire Promise.all rejects.
Checkpoint: If one promise inside Promise.all fails, what happens to the entire operation?
- →It continues with the successful ones
- →It immediately rejects the whole Promise.all
Beyond Promise.all: allSettled() waits for ALL regardless of failures. race() resolves/rejects with the FIRST to finish. any() resolves with the FIRST success (ignores rejections). Each solves a different concurrency pattern.
Real-world pattern: manage loading, data, and error states with async/await. This is the exact pattern used in React, Vue, and every modern framework for data fetching.
Checkpoint: How do you capture the value returned from an awaited Promise?
- →Using a .then() callback
- →Assigning it to a const/let variable
Modern JavaScript (ES modules) supports top-level await — you can use await outside any function at the module's root. No async wrapper needed. This is the standard in Node.js ESM and modern browsers.
Async/Await mastered: async wraps returns in Promises, await pauses and unwraps, try/catch handles all errors, Promise.all for parallel execution, and allSettled/race/any for advanced patterns. Next: the Fetch API.
Level Up 🚀
Advanced cheat sheets, SEO tricks, and interview prep for this topic.
Browser Support
Fully supported.
Fully supported.
Fully supported.
Fully supported.
Accessibility (A11y)
1Announce Loading and Error States from Async Operations to Screen Readers
Fetching data with async/await often changes the UI between loading, loaded, and error states, but a purely visual spinner or error message is invisible to screen reader users. Wrap the status region in an `aria-live="polite"` container so state changes ('Loading...', 'Failed to load data') are announced automatically.
SEO Implications
- 1
Awaited Data That Renders After Initial Load Can Be Invisible to Crawlers
If a page's main content depends on an async/await fetch that resolves only after the initial HTML is delivered, crawlers that don't fully execute and wait on JavaScript may index a page with missing content. Server-side rendering or static generation that awaits the data before responding avoids this gap.
Best Practices
Use Promise.all() for Independent Async Operations Instead of Sequential Awaits
Awaiting one request, then another, forces the second to wait even though it doesn't depend on the first's result — this needlessly adds their durations together. Fire independent requests concurrently and await them together with Promise.all() so the total time is the duration of the slowest one.
Always Wrap Await Calls in Try/Catch (or Handle Rejections Explicitly)
An awaited Promise that rejects without a surrounding try/catch throws inside the async function and, if uncaught anywhere in the call chain, can crash a Node process or surface as an unhandled promise rejection in the browser. Every await that can fail should be inside a try/catch or have its rejection handled by the caller.
Frequent Bugs
Awaiting independent async calls one after another, unintentionally serializing requests that could run in parallel.
`const a = await fetchA(); const b = await fetchB();` makes fetchB wait for fetchA to finish even if they don't depend on each other. Start both first (`const pA = fetchA(); const pB = fetchB();`) and await them together with `Promise.all([pA, pB])` to run them concurrently.
Using Promise.all() when partial failures should be tolerated, and losing all successful results because one request rejected.
Promise.all() rejects entirely the instant any one of its promises rejects, discarding every other result even if they succeeded. Use Promise.allSettled() when you need to know the outcome of every request regardless of individual failures.
Real-World Examples
Loading a Dashboard's Data with Promise.all and a Loading/Error State
A dashboard needed to fetch a user's profile and their usage stats concurrently, show a loading indicator while both were in flight, and display a clear error message if either request failed.
async function loadDashboard() {
let loading = true, data = null, error = null;
try {
const [users, stats] = await Promise.all([fetchUsers(), fetchStats()]);
data = { users, stats };
} catch (err) {
error = err.message;
} finally {
loading = false;
}
return { loading, data, error };
}