Async / Await
async/await is syntactic sugar over Promises that makes asynchronous code look and feel synchronous. Write cleaner async code without .then() chains.
The Problem with .then() Chains
Promises are great, but deep chains can still get messy, especially with conditionals:
// With promises
function getFullUserInfo(userId) {
return fetchUser(userId)
.then(user => {
if (!user) throw new Error("User not found");
return fetchProfile(user.id)
.then(profile => {
return fetchSettings(user.id)
.then(settings => {
return { user, profile, settings };
});
});
});
}
Enter async/await
async/await lets you write the same logic in a way that reads like synchronous code:
async function getFullUserInfo(userId) {
const user = await fetchUser(userId);
if (!user) throw new Error("User not found");
const profile = await fetchProfile(user.id);
const settings = await fetchSettings(user.id);
return { user, profile, settings };
}
Much cleaner! Same power, better readability.
How It Works
asyncmarks a function as asynchronous โ it always returns a Promiseawaitpauses execution of theasyncfunction until the Promise resolves- The
awaitexpression evaluates to the resolved value
async function example() {
console.log("1: before await");
const result = await Promise.resolve(42);
console.log("2: after await, result:", result);
return "done";
}
example().then(val => console.log("3:", val));
console.log("4: this runs before the await resolves");
// Output order:
// 1: before await
// 4: this runs before the await resolves
// 2: after await, result: 42
// 3: done
await only pauses the async function โ the rest of the program keeps running.
Error Handling with try/catch
Use try/catch just like synchronous code:
async function fetchUserData(userId) {
try {
const user = await fetchUser(userId);
const posts = await fetchPosts(user.id);
return { user, posts };
} catch (error) {
console.error("Failed to fetch user data:", error.message);
return null; // or re-throw: throw error;
} finally {
console.log("Fetch complete");
}
}
โ ๏ธAlways handle errors in async functions
An unhandled rejection in an async function (a thrown error without a catch) will cause an UnhandledPromiseRejectionWarning. Either use try/catch or attach .catch() when calling the async function.
// Option 1: try/catch inside
async function doWork() {
try { await riskyOperation(); }
catch (e) { /* handle */ }
}
// Option 2: .catch() on the call
doWork().catch(e => console.error(e));
Parallel vs Sequential
Understand the difference โ this is a common performance mistake:
// โ Sequential โ each awaits the previous (300ms total)
async function sequential() {
const a = await fetchA(); // 100ms
const b = await fetchB(); // 100ms
const c = await fetchC(); // 100ms
return [a, b, c];
}
// โ Parallel โ all start simultaneously (100ms total)
async function parallel() {
const [a, b, c] = await Promise.all([
fetchA(), // starts immediately
fetchB(), // starts immediately
fetchC(), // starts immediately
]);
return [a, b, c];
}
Only use sequential await when each operation depends on the previous result:
// Sequential is correct here โ each step needs the previous result
async function process() {
const order = await createOrder(); // needs to exist first
const payment = await processPayment(order.id); // needs order
const receipt = await sendReceipt(payment.id); // needs payment
return receipt;
}
await with Promise.all
Combine await with Promise.all for clean parallel code:
async function loadDashboard(userId) {
// All three start simultaneously
const [user, notifications, stats] = await Promise.all([
getUser(userId),
getNotifications(userId),
getStats(userId),
]);
return { user, notifications, stats };
}
Top-Level await (ES2022)
In ES modules, you can use await at the top level without wrapping in an async function:
// In a .mjs file or module:
const data = await fetch("/api/data").then(r => r.json());
console.log(data);
for await...of โ Async Iteration
Iterate over async iterables or arrays of Promises:
async function processItems(ids) {
for await (const id of ids) {
const data = await fetch(`/api/items/${id}`);
console.log(await data.json());
}
}
// Or with an async generator
async function* fetchPages(urls) {
for (const url of urls) {
const response = await fetch(url);
yield await response.json();
}
}
Async Functions Always Return a Promise
Even if you return a plain value:
async function getNumber() {
return 42;
}
// Returns a Promise, not 42
const result = getNumber();
console.log(result); // Promise { <fulfilled>: 42 }
// To get the value:
result.then(v => console.log(v)); // 42
// Or inside another async function:
async function main() {
const num = await getNumber();
console.log(num); // 42
}
Practical Patterns
Retry with backoff
async function fetchWithRetry(url, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return await response.json();
} catch (error) {
if (attempt === maxRetries) throw error;
const wait = 2 ** attempt * 100; // exponential backoff
console.log(`Attempt ${attempt} failed. Retrying in ${wait}ms...`);
await new Promise(resolve => setTimeout(resolve, wait));
}
}
}
Timeout
function withTimeout(promise, ms) {
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error(`Timed out after ${ms}ms`)), ms)
);
return Promise.race([promise, timeout]);
}
async function main() {
try {
const data = await withTimeout(fetch("/api/slow"), 5000);
console.log(data);
} catch (e) {
console.error(e.message);
}
}
Key Takeaways
asyncmakes a function return a Promise;awaitwaits for a Promise to resolve- Use
try/catch/finallyfor error handling โ much cleaner than.catch()chains - Independent operations should run in parallel with
Promise.allโ sequentialawaitis a performance trap awaitonly pauses the currentasyncfunction, not the whole program- Async functions always return Promises, even when returning plain values
Ready to test your knowledge?
Take a quiz on what you just learned.