Why Async/Await Exists
JavaScript is single-threaded — only one thing runs at a time. But many operations (HTTP requests, file reads, database queries) take time. Async/await gives you a way to wait for those operations without blocking the entire thread.
Under the hood, async/await is syntax sugar over Promises. Understanding Promises makes async/await fully transparent.
Promises Refresher
A Promise represents a value that may not be available yet:
const promise = new Promise((resolve, reject) => {
setTimeout(() => resolve('done!'), 1000);
});
promise
.then(value => console.log(value)) // "done!" after 1s
.catch(error => console.error(error));
async/await Syntax
async marks a function as asynchronous. Inside it, await pauses execution until the Promise resolves:
async function fetchUser(id) {
const res = await fetch(`/api/users/${id}`);
const user = await res.json();
return user;
}
This reads like synchronous code but runs asynchronously. Calling fetchUser returns a Promise.
Error Handling
try/catch — The Standard Way
async function getUser(id) {
try {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error(`HTTP error: ${res.status}`);
return await res.json();
} catch (error) {
console.error('Failed to fetch user:', error);
return null;
}
}
Wrapping Promises (Utility Pattern)
async function safe(promise) {
try {
const data = await promise;
return [null, data];
} catch (error) {
return [error, null];
}
}
const [error, user] = await safe(fetchUser(id));
if (error) return handleError(error);
Sequential vs Parallel Execution
Sequential (each waits for the previous)
// Total time: 1s + 1s + 1s = 3 seconds
const user = await fetchUser(id);
const posts = await fetchPosts(user.id);
const comments = await fetchComments(posts[0].id);
Parallel (all start at the same time)
// Total time: max(1s, 1s, 1s) = 1 second
const [user, settings, notifications] = await Promise.all([
fetchUser(id),
fetchSettings(id),
fetchNotifications(id),
]);
Use Promise.all whenever operations are independent. This is one of the biggest performance wins in async code.
Promise.allSettled — When You Need All Results
Promise.all rejects if any Promise rejects. Promise.allSettled waits for all, regardless:
const results = await Promise.allSettled([
fetchUser(1),
fetchUser(2), // might fail
fetchUser(3),
]);
results.forEach(result => {
if (result.status === 'fulfilled') {
console.log(result.value);
} else {
console.error(result.reason);
}
});
Promise.race — First to Resolve Wins
Useful for timeouts:
function withTimeout(promise, ms) {
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), ms)
);
return Promise.race([promise, timeout]);
}
const user = await withTimeout(fetchUser(id), 5000);
Common Mistakes
Forgetting await:
// Bug: user is a Promise, not the resolved value
const user = fetchUser(id);
console.log(user.name); // undefined
Awaiting in a loop sequentially when parallel would work:
// Slow
for (const id of ids) {
await processUser(id); // one at a time
}
// Fast
await Promise.all(ids.map(id => processUser(id)));
Conclusion
Async/await is built on Promises — never forget that. Master the three critical patterns: sequential awaits for dependent operations, Promise.all for independent parallel operations, and try/catch for errors. These three patterns cover nearly every real-world async scenario you'll encounter.