What Is async/await?
Introduced in ES2017, async/await is syntactic sugar built on top of JavaScript Promises. It allows you to write asynchronous code that looks synchronous, making it far easier to read and reason about compared to nested callbacks or chained .then() calls. Under the hood, the same Promise machinery drives everything — async/await is just a cleaner way to interact with it.
The Basics: async Functions and await
Any function prefixed with the async keyword automatically returns a Promise. Inside an async function, you can use the await keyword to pause execution until a Promise resolves:
async function fetchUserData(userId) {
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
return data;
}
Without async/await, the equivalent code using Promises requires chaining:
function fetchUserData(userId) {
return fetch(`/api/users/${userId}`)
.then(response => response.json())
.then(data => data);
}
Both are functionally identical, but the async/await version is far more readable — especially as the logic grows more complex.
Error Handling with try/catch
One of the biggest advantages of async/await is how naturally it integrates with standard JavaScript error handling. Use a try/catch block exactly as you would for synchronous code:
async function loadPost(id) {
try {
const response = await fetch(`/api/posts/${id}`);
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('Failed to load post:', error);
return null;
}
}
This is much cleaner than chaining .catch() onto Promise chains, and it handles both network errors and application-level errors in one place.
Common Pitfalls to Avoid
1. Forgetting to await
If you forget the await keyword, you'll receive a Promise object instead of the resolved value — and your code will produce subtle, hard-to-debug bugs:
// ❌ Wrong — data is a Promise, not the actual data
const data = fetchUserData(1);
// ✅ Correct
const data = await fetchUserData(1);
2. Sequential await When Parallel Is Possible
Awaiting calls sequentially when they're independent is a performance mistake. Each call will wait for the previous one to complete unnecessarily:
// ❌ Slow — waits for user, then waits for posts
const user = await getUser(id);
const posts = await getPosts(id);
// ✅ Fast — both requests fire simultaneously
const [user, posts] = await Promise.all([getUser(id), getPosts(id)]);
3. Using await in Array.forEach
forEach doesn't understand async callbacks — the awaits inside it won't work as expected. Use for...of for sequential async iteration, or Promise.all with map for parallel:
// ❌ Doesn't work as expected
items.forEach(async (item) => {
await processItem(item);
});
// ✅ Sequential
for (const item of items) {
await processItem(item);
}
// ✅ Parallel
await Promise.all(items.map(item => processItem(item)));
Top-Level await (ES2022+)
In modern JavaScript modules, you can now use await at the top level — outside of any async function. This is particularly useful in configuration files, build scripts, and Node.js modules:
// In an ES module (.mjs or with "type": "module")
const config = await fetch('/config.json').then(r => r.json());
console.log(config);
async/await Best Practices Summary
- Always handle errors with try/catch — unhandled Promise rejections crash Node.js processes and can cause silent failures in browsers.
- Use Promise.all for independent concurrent operations to minimize total wait time.
- Avoid mixing async/await and raw Promise chains in the same function — choose one style for consistency.
- Be explicit about return values — remember that async functions always return a Promise, even if you return a plain value.
- Use async/await in loops carefully — understand whether sequential or parallel execution is your intent.
Async/await is now the dominant pattern for handling asynchronous logic in modern JavaScript. Understanding it deeply — including its relationship to Promises and its edge cases — separates competent developers from expert ones.