Async & Await
Syntactic Sugar Over Promises
async/await makes asynchronous code read like synchronous code. It does not replace promises — it is built on top of them.
// With promises
function getUser(id) {
return fetch(`/api/users/${id}`)
.then(response => response.json());
}
// With async/await
async function getUser(id) {
const response = await fetch(`/api/users/${id}`);
return response.json();
}
async Functions Always Return a Promise
async function greet() { return "Hello"; }
greet().then(msg => console.log(msg)); // "Hello"
async function failing() { throw new Error("Broke"); }
failing().catch(e => console.error(e.message)); // "Broke"
await Pauses Until the Promise Settles
await can only be used inside an async function (or at the top level of a module). It pauses that function until the promise fulfills or rejects. The rest of the program continues.
async function loadDashboard(userId) {
const user = await fetchUser(userId);
const orders = await fetchOrders(user.id);
console.log(`${user.name} has ${orders.length} orders`);
}
Error Handling with try/catch
async function loadProfile(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const user = await response.json();
displayProfile(user);
} catch (error) {
console.error("Failed:", error.message);
displayErrorMessage("Could not load profile.");
}
}
Granular Error Handling
async function checkout(cartId) {
let cart;
try { cart = await fetchCart(cartId); }
catch { showError("Could not load cart"); return; }
let payment;
try { payment = await processPayment(cart); }
catch { showError("Payment failed"); return; }
try { await sendConfirmationEmail(payment.orderId); }
catch (e) { console.warn("Email failed:", e.message); } // non-critical
showSuccess(`Order ${payment.orderId} confirmed`);
}
Parallel Async with Promise.all
The most common performance mistake: running independent operations sequentially.
// Bad: sequential — 1500ms total
const user = await fetchUser(id); // 500ms
const orders = await fetchOrders(id); // 500ms
const settings = await fetchSettings(id); // 500ms
// Good: parallel — 500ms total
const [user, orders, settings] = await Promise.all([
fetchUser(id), fetchOrders(id), fetchSettings(id)
]);
Mixed Sequential & Parallel
async function loadOrderPage(userId) {
const user = await fetchUser(userId); // need user first
const [orders, prefs] = await Promise.all([ // these are independent
fetchOrders(user.id), fetchPreferences(user.id)
]);
return { user, orders, prefs };
}
Async Iteration
// Sequential: one at a time
async function processSequentially(urls) {
for (const url of urls) {
const response = await fetch(url);
process(await response.json());
}
}
// Parallel: all at once
async function processInParallel(urls) {
return Promise.all(urls.map(async url => {
const response = await fetch(url);
return response.json();
}));
}
Top-Level Await
In ES modules, await works outside async functions.
// config.js (ES module)
const response = await fetch("/api/config");
export const config = await response.json();
Any module that imports it waits for the promise to resolve.
Async/Await vs Raw Promises
Use async/await for sequential steps with branching. Use raw promises for combinators and utilities.
// Raw promises: cleaner for composition
function fetchWithTimeout(url, ms) {
return Promise.race([
fetch(url),
new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout")), ms))
]);
}
// async/await: cleaner for sequential logic with retry
async function retryFetch(url, attempts) {
for (let i = 0; i < attempts; i++) {
try {
const response = await fetch(url);
if (response.ok) return response;
} catch (e) { if (i === attempts - 1) throw e; }
}
}
Real-World Pattern: Retry with Backoff
async function fetchWithRetry(url, { maxRetries = 3, baseDelay = 1000 } = {}) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(url);
if (response.ok) return response;
if (response.status >= 400 && response.status < 500) {
throw new Error(`Client error: ${response.status}`);
}
throw new Error(`Server error: ${response.status}`);
} catch (error) {
if (attempt === maxRetries) throw error;
const delay = baseDelay * Math.pow(2, attempt);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
Common Pitfalls
Sequential await for independent operations. Use Promise.all when operations do not depend on each other.
Using await inside forEach. forEach does not await async callbacks. The loop finishes immediately.
// Bug: forEach fires all at once, doesn't wait
urls.forEach(async url => { await fetch(url); });
// Fix: for...of for sequential
for (const url of urls) { await fetch(url); }
// Fix: Promise.all for parallel
await Promise.all(urls.map(async url => fetch(url)));
Swallowing errors in try/catch. Catching an error and doing nothing hides bugs. At minimum, log it.
Unnecessary async. If a function just returns a promise without using await, drop the async keyword.
Key Takeaways
async/awaitis syntactic sugar over promises.asyncfunctions always return a promise.- Use
try/catchfor error handling. Granular blocks let you handle different failures differently. - Use
await Promise.all([...])for independent parallel operations. Sequentialawaitis for dependent steps. forEachdoes not work withawait. Usefor...oforPromise.allwithmap.- Top-level
awaitworks in ES modules for initialization. - Choose async/await for readability, raw promises for composition.