Why Async/Await Changed Everything
Before async/await landed in ES2017, JavaScript developers wrestled with callback hell and promise chains that could spiral into unreadable pyramids of doom. Async/await didn't replace promises — it gave us a cleaner, more human way to write them. If you've ever stared at a .then().then().catch() chain and lost track of what's happening, this guide is for you.
The Basics: What Are async and await?
At its core, async is a keyword you place before a function declaration. It tells JavaScript: "This function will return a promise." The await keyword can only be used inside an async function, and it pauses execution until the awaited promise resolves.
async function brewCoffee() {
const beans = await fetchBeans();
const ground = await grindBeans(beans);
return brew(ground);
}
Each await waits for the previous step to finish before moving on — much like a barista who won't pour milk before the espresso is pulled.
Error Handling with try/catch
One of the biggest wins with async/await is how naturally it integrates with try/catch blocks:
async function getUser(id) {
try {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) throw new Error('User not found');
const data = await response.json();
return data;
} catch (error) {
console.error('Failed to fetch user:', error.message);
}
}
This is far more readable than chaining .catch() handlers on every promise.
Running Promises in Parallel
A common mistake is awaiting promises sequentially when they could run in parallel. Consider this inefficient pattern:
- Slow:
const a = await fetchA();thenconst b = await fetchB();— these run one after the other. - Fast: Use
Promise.all()to run them simultaneously.
// Parallel — much faster
const [userData, orderData] = await Promise.all([
fetchUser(id),
fetchOrders(id)
]);
This is the equivalent of ordering your coffee and pastry at the same time rather than waiting for one before asking for the other.
Common Pitfalls to Avoid
- Forgetting to await: If you call an async function without
await, you get a pending promise back, not a value. - Using await in loops incorrectly: A
for...ofloop withawaitruns sequentially. UsePromise.all(array.map(...)))for parallel iteration. - Unhandled rejections: Always wrap your async logic in try/catch or attach a
.catch()to the top-level promise. - Top-level await: Outside of ES modules,
awaitcan't be used at the top level. Wrap it in an immediately invoked async function if needed.
async/await with the Fetch API
Here's a complete, real-world example of fetching data from a REST API with proper error handling:
async function fetchArticles(category) {
try {
const res = await fetch(`https://api.example.com/articles?cat=${category}`);
if (!res.ok) throw new Error(`HTTP error: ${res.status}`);
const articles = await res.json();
return articles;
} catch (err) {
console.error('Could not load articles:', err);
return [];
}
}
Key Takeaways
- Async/await is syntactic sugar over promises — understanding promises first is essential.
- Always handle errors with try/catch inside async functions.
- Use
Promise.all()when operations are independent to improve performance. - Async/await makes asynchronous code read like synchronous code — which is a huge win for maintainability.
Once you get the rhythm of async/await, you'll find it becomes second nature — as natural as reaching for your morning brew before opening your IDE.