Async / Await
Write asynchronous code that reads like synchronous code — the modern standard.
1. Introduction
async/await is syntactic sugar over Promises. It lets you write async code that looks and reads like regular synchronous code — no .then() chains, no callbacks. Under the hood it still uses Promises, so everything you learned in Lesson 2 applies. async/await is the dominant pattern in modern JavaScript.
2. Theory
2.1 The async keyword
// async marks a function as asynchronous
// It always returns a Promise, even if you return a plain value
async function greet(name) {
return `Hello, ${name}!`; // wrapped in Promise.resolve() automatically
}
const result = greet('Alice');
console.log(result); // Promise { 'Hello, Alice!' }
result.then(msg => console.log(msg)); // "Hello, Alice!"
// async arrow function
const square = async n => n * n;
// async method
class Api {
async getUser(id) { ... }
}
2.2 The await keyword
// await pauses execution of the async function until the Promise settles
// It can only be used INSIDE an async function
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function example() {
console.log('Start');
await delay(1000); // pause for 1 second
console.log('After 1 second');
await delay(500);
console.log('After 0.5 more seconds');
}
example();
// Output: "Start" → (1s) → "After 1 second" → (0.5s) → "After 0.5 more seconds"
// Other code can run during the waits!
2.3 async/await vs .then() — same result
// Promise chain
function loadData() {
return fetchUser(1)
.then(user => fetchProfile(user.id))
.then(profile => fetchPosts(profile.id))
.then(posts => posts);
}
// async/await — identical behaviour, much more readable
async function loadData() {
const user = await fetchUser(1);
const profile = await fetchProfile(user.id);
const posts = await fetchPosts(profile.id);
return posts;
}
2.4 Error handling with try/catch
// .then() approach
fetchUser(1)
.then(user => processUser(user))
.catch(err => console.error(err));
// async/await — use regular try/catch
async function handleUser() {
try {
const user = await fetchUser(1);
const result = await processUser(user);
return result;
} catch (err) {
console.error('Error:', err.message);
return null; // or re-throw: throw err;
} finally {
console.log('Always runs');
}
}
// Multiple specific error types
async function loadResource(id) {
try {
const user = await fetchUser(id);
return user;
} catch (err) {
if (err.message.includes('not found')) {
return null; // expected — handle gracefully
}
throw err; // unexpected — re-throw for caller to handle
}
}
2.5 Parallel execution with async/await
// Sequential — slow (each waits for the previous)
async function sequential() {
const users = await fetchUsers(); // wait
const products = await fetchProducts(); // wait
const orders = await fetchOrders(); // wait
return { users, products, orders }; // total = sum of all times
}
// Parallel — fast (all start simultaneously)
async function parallel() {
const [users, products, orders] = await Promise.all([
fetchUsers(),
fetchProducts(),
fetchOrders()
]);
return { users, products, orders }; // total = slowest of the three
}
// Conditional parallel — start some in parallel, await when needed
async function smart() {
const userPromise = fetchUser(1); // started
const settingsPromise = fetchSettings(); // started
// Do some synchronous work while both are fetching
const config = processLocalConfig();
const user = await userPromise; // await both
const settings = await settingsPromise;
return { user, settings, config };
}
2.6 Async in loops
const ids = [1, 2, 3, 4, 5];
// WRONG — Promise.all ignores await inside forEach
async function wrong() {
ids.forEach(async id => {
const user = await fetchUser(id); // await inside forEach does nothing useful
console.log(user);
});
}
// Sequential — one at a time (use for...of)
async function sequential() {
for (const id of ids) {
const user = await fetchUser(id); // waits for each
console.log(user.name);
}
}
// Parallel — all at once
async function parallel() {
const users = await Promise.all(ids.map(id => fetchUser(id)));
users.forEach(u => console.log(u.name));
}
// Parallel with limit (process N at a time)
async function batched(ids, batchSize = 2) {
for (let i = 0; i < ids.length; i += batchSize) {
const batch = ids.slice(i, i + batchSize);
const results = await Promise.all(batch.map(id => fetchUser(id)));
results.forEach(u => console.log(u.name));
}
}
2.7 Top-level await (modern)
// In ES2022+, await can be used at the top level of ES modules
// (no async wrapper needed in module files)
// data.js (an ES module)
const response = await fetch('/api/data.json');
export const data = await response.json();
// This works in modern browsers and Node.js 14+
// Not available in classic scripts — only in type="module" contexts
2.8 IIFE for async in non-module contexts
// Can't use top-level await in a regular script? Wrap in an async IIFE
(async () => {
try {
const data = await fetch('/api/data').then(r => r.json());
console.log(data);
} catch (err) {
console.error(err);
}
})();
3. Real World Example
// Authentication flow with async/await
async function authenticate(email, password) {
const loginBtn = document.querySelector('#login-btn');
const errorEl = document.querySelector('#error-msg');
loginBtn.disabled = true;
loginBtn.textContent = 'Signing in...';
errorEl.textContent = '';
try {
// Step 1: login
const { token } = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
}).then(r => {
if (!r.ok) throw new Error('Invalid credentials');
return r.json();
});
// Step 2: get user profile using token
const user = await fetch('/api/me', {
headers: { 'Authorization': `Bearer ${token}` }
}).then(r => r.json());
// Step 3: parallel load of dashboard data
const [notifications, settings] = await Promise.all([
fetch('/api/notifications').then(r => r.json()),
fetch('/api/settings').then(r => r.json())
]);
sessionStorage.setItem('token', token);
renderDashboard({ user, notifications, settings });
} catch (err) {
errorEl.textContent = err.message || 'Sign in failed. Please try again.';
} finally {
loginBtn.disabled = false;
loginBtn.textContent = 'Sign In';
}
}
4. Code Example
<input id="user-id" type="number" min="1" max="10" value="1">
<button id="load">Load User Data</button>
<div id="status"></div>
<div id="output"></div>
<script>
// Simulated async APIs
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function getUser(id) {
await delay(300);
if (id > 5) throw new Error(`No user with id ${id}`);
return { id, name: `User ${id}`, email: `user${id}@example.com` };
}
async function getPosts(userId) {
await delay(200);
return [
{ id: 1, title: `Post A by user ${userId}` },
{ id: 2, title: `Post B by user ${userId}` }
];
}
const status = document.querySelector('#status');
const output = document.querySelector('#output');
document.querySelector('#load').addEventListener('click', async () => {
const id = Number(document.querySelector('#user-id').value);
status.textContent = 'Loading...';
output.innerHTML = '';
try {
// Parallel fetch — user and posts at the same time
const [user, posts] = await Promise.all([getUser(id), getPosts(id)]);
status.textContent = `Loaded in ${Date.now()}ms`;
const userDiv = document.createElement('div');
userDiv.textContent = `${user.name} — ${user.email}`;
const ul = document.createElement('ul');
posts.forEach(post => {
const li = document.createElement('li');
li.textContent = post.title;
ul.appendChild(li);
});
output.append(userDiv, ul);
} catch (err) {
status.textContent = '';
const p = document.createElement('p');
p.style.color = 'red';
p.textContent = 'Error: ' + err.message;
output.appendChild(p);
}
});
</script>
5. Code Breakdown
async event listener
The click handler is marked async. This allows using await inside it. Errors thrown inside an async event listener are caught by the try/catch — they don't go unhandled.
Promise.all with destructuring
Both getUser and getPosts start simultaneously. await Promise.all([...]) waits for both. Destructuring [user, posts] cleanly names each result.
Error thrown in getUser
When id > 5, getUser throws. This rejects the Promise returned by the async function. Promise.all then rejects, causing the outer await to throw, which the catch block catches.
6. Common Mistakes
Mistake 1 — Using await without async
// SyntaxError — await only valid inside async function
function load() {
const data = await fetch('/api'); // SyntaxError
}
// Fix
async function load() {
const data = await fetch('/api');
}
Mistake 2 — Sequential when parallel is better
// Slow — 3 × 500ms = 1500ms
async function slow() {
const a = await fetchA(); // wait 500ms
const b = await fetchB(); // wait 500ms
const c = await fetchC(); // wait 500ms
return { a, b, c };
}
// Fast — max(500, 500, 500) = 500ms
async function fast() {
const [a, b, c] = await Promise.all([fetchA(), fetchB(), fetchC()]);
return { a, b, c };
}
Mistake 3 — Forgetting to await
async function save(data) {
const result = saveToDb(data); // forgot await — result is a Promise, not data!
console.log(result); // Promise { <pending> } — not useful
}
// Fix
async function save(data) {
const result = await saveToDb(data);
console.log(result); // actual data
}
Mistake 4 — await in forEach
// Bug — async forEach callbacks are not awaited
async function processAll(items) {
items.forEach(async item => {
await process(item); // this runs, but forEach doesn't wait for it
});
console.log('Done'); // logs before any items are processed!
}
// Fix — use for...of
async function processAll(items) {
for (const item of items) {
await process(item);
}
console.log('Done'); // logs after all processed
}
7. Best Practices
- Use async/await for sequential async operations — cleaner than .then() chains.
- Use Promise.all for parallel operations — don't await each one separately when they're independent.
- Always use try/catch with async/await — unhandled async errors can be invisible.
- Don't await in forEach — use for...of (sequential) or Promise.all + map (parallel).
- Re-throw errors you can't handle — don't silently swallow errors you don't know how to fix.
- Use finally for cleanup — hide spinners, re-enable buttons, regardless of success or failure.
8. Practice Exercise
- Convert this .then() chain to async/await:
fetch('/api/users').then(r => r.json()).then(users => users.filter(u => u.active)).then(console.log).catch(console.error). - Write an
async function loadAll(ids)that fetches all users in parallel and returns the array of results. - Write a
retry(fn, attempts)function that calls an async function and retries up to N times on failure before throwing.
9. Assignment
Build a "GitHub Profile Viewer" (using the real GitHub API).
- Input: GitHub username. On submit, fetch
https://api.github.com/users/{username}and display name, bio, followers, public repos, and avatar. - Also fetch
https://api.github.com/users/{username}/repos?sort=updated&per_page=5in parallel and display the 5 most recently updated repos. - Show a loading state during fetch, clear it when done.
- Handle errors: user not found (404), rate limit exceeded (403), network error.
- Use async/await throughout — no .then() chains.
Deliverable: One HTML file.
10. Interview Questions
- What does the async keyword do?
It marks a function as asynchronous. The function always returns a Promise — if it returns a plain value, that value is wrapped in Promise.resolve(). It also enables the use of await inside the function. - What does await do?
It pauses execution of the async function until the Promise settles. If the Promise fulfills, await returns the value. If it rejects, await throws the error (catchable by try/catch). Other code continues running during the pause. - What is the difference between sequential and parallel async/await?
Sequential: await each one in turn — total time = sum of all durations. Parallel: start all Promises then await Promise.all — total time = slowest single operation. Always prefer parallel when operations are independent. - How do you handle errors with async/await?
Wrap await calls in try/catch blocks. The catch block receives the rejection reason as an Error object. Use finally for cleanup code that must run regardless of success or failure.
11. Additional Resources
- MDN — async function
- MDN — await
- javascript.info — Async/await
- MDN — Top-level await