Async/Await in JavaScript Explained
Async/await makes asynchronous code look synchronous. Here's how it actually works.
The Problem It Solves
JavaScript is single-threaded. When you fetch data, you can't freeze everything waiting for it.
Old way (callbacks):
fetchUser(id, (user) => {
fetchPosts(user.id, (posts) => {
fetchComments(posts[0].id, (comments) => {
// Callback hell
});
});
});
Modern way (async/await):
const user = await fetchUser(id);
const posts = await fetchPosts(user.id);
const comments = await fetchComments(posts[0].id);
Same behavior, readable code.
The Basics
async marks a function as asynchronous:
async function getData() {
return "hello"; // Automatically wrapped in Promise
}
await pauses until a Promise resolves:
async function fetchUser() {
const response = await fetch('/api/user');
const data = await response.json();
return data;
}
Error Handling
Use try/catch like synchronous code:
async function fetchUser() {
try {
const response = await fetch('/api/user');
if (!response.ok) throw new Error('Failed to fetch');
return await response.json();
} catch (error) {
console.error('Error:', error);
return null;
}
}
Running in Parallel
Sequential (slow):
const user = await fetchUser(); // Wait...
const posts = await fetchPosts(); // Then wait...
const comments = await fetchComments(); // Then wait...
Parallel (fast):
const [user, posts, comments] = await Promise.all([
fetchUser(),
fetchPosts(),
fetchComments()
]);
Use Promise.all when requests don't depend on each other.
Common Mistakes
1. Forgetting await
// Bug: response is a Promise, not data
const response = fetch('/api/user');
console.log(response); // Promise { }
// Fix:
const response = await fetch('/api/user');
2. await in loops (sequential when parallel is possible)
// Slow: waits for each one
for (const id of ids) {
const user = await fetchUser(id);
}
// Fast: runs all at once
const users = await Promise.all(ids.map(id => fetchUser(id)));
3. Not handling errors
// Unhandled rejection if fetch fails
const data = await fetch('/api/data');
// Better: always handle errors
const data = await fetch('/api/data').catch(e => null);
Async in Different Contexts
Top-level await (ES modules):
// Works in ES modules
const config = await loadConfig();
In event handlers:
button.addEventListener('click', async () => {
const result = await saveData();
});
In array methods (careful!):
// forEach doesn't wait for async
items.forEach(async (item) => {
await processItem(item); // These run simultaneously!
});
// Use for...of for sequential
for (const item of items) {
await processItem(item);
}
Summary
- async functions always return Promises
- await pauses until the Promise resolves
- Use try/catch for errors
- Use Promise.all for parallel operations
- Avoid await in loops when parallel is possible
Further Reading
For the complete reference, see MDN's guide to Promises and MDN's async function documentation.