Async/Await vs Promises: The JavaScript Story Nobody Tells You
Async/Await vs Promises: The JavaScript Story Nobody Tells You
Promises and async/await are often framed as competitors — but they’re actually teammates. This story-driven guide explains their relationship, shows practical examples, and highlights common beginner mistakes with clear fixes.
By Udbhav ·
1. The Callback Era (The Problem)
When I first learned JavaScript promises, I was thrilled. Before promises, asynchronous code looked like nested callbacks — famously known as callback hell. Readability suffered and error handling was clumsy.
// callback hell example
function getUser(callback) {
setTimeout(() => {
callback(null, { id: 1, name: "Alice" });
}, 1000);
}
function getOrders(userId, callback) {
setTimeout(() => {
callback(null, ["Order1", "Order2"]);
}, 1000);
}
getUser((err, user) => {
if (err) return console.error(err);
getOrders(user.id, (err, orders) => {
if (err) return console.error(err);
console.log("Orders:", orders);
});
});
2. Promises to the Rescue
Promises flattened that nesting and gave us a chainable API.
// Promises example
function getUser() {
return new Promise(resolve => {
setTimeout(() => resolve({ id: 1, name: "Alice" }), 1000);
});
}
function getOrders(userId) {
return new Promise(resolve => {
setTimeout(() => resolve(["Order1", "Order2"]), 1000);
});
}
getUser()
.then(user => getOrders(user.id))
.then(orders => console.log("Orders:", orders))
.catch(err => console.error(err));
3. The Async/Await Revolution
Introduced in ES2017, async/await is syntactic sugar over promises. It lets you write asynchronous code that looks synchronous — making it easier to reason about.
// async/await example
async function fetchOrders() {
try {
const user = await getUser();
const orders = await getOrders(user.id);
console.log("Orders:", orders);
} catch (err) {
console.error(err);
}
}
fetchOrders();
4. The Truth Nobody Tells You
Async/await does not replace promises — it builds on them. `await` expects a promise; `async` functions always return a promise.
// async returns a promise
async function hello() {
return "Hello World";
}
hello().then(msg => console.log(msg)); // "Hello World"
5. When to Use Promises vs Async/Await
Both are useful. Choose based on parallelism vs sequential flow.
Use Promises (and Promise.all) when:
- You have multiple independent async tasks and want them to run in parallel.
- You prefer a pipeline-style chain for transforms.
// Promise.all example — parallel fetches
Promise.all([getUser(), getOrders(1)])
.then(([user, orders]) => console.log(user, orders))
.catch(console.error);
Use Async/Await when:
- Tasks are sequential and depend on previous results.
- You want clearer try/catch style error handling.
// sequential with async/await
async function main() {
const user = await getUser();
const orders = await getOrders(user.id);
console.log(user, orders);
}
6. Mixing Both (The Secret Sauce)
In real projects you’ll mix patterns. A common idiom: use async/await for top-level readability, and Promise.all for parallelism within it.
// best of both: Promise.all inside async/await
async function main() {
const user = await getUser();
const [orders, profile] = await Promise.all([
getOrders(user.id),
getProfile(user.id)
]);
console.log(orders, profile);
}
7. Common Mistakes Beginners Make
Forgetting to await
// ❌ missing await -> returns a Promise
async function main() {
const user = getUser(); // missing await
console.log(user); // Promise { }
}
// ✅ include await when you need the result
async function main() {
const user = await getUser();
console.log(user);
}
Awaiting in a loop (slow)
// ❌ sequential (slow)
for (let id of [1,2,3]) {
const user = await getUser(id);
console.log(user);
}
// ✅ parallel using map + Promise.all
const promises = [1,2,3].map(id => getUser(id));
const users = await Promise.all(promises);
console.log(users);
Not handling rejection in Promise.all
// ❌ one reject breaks all
await Promise.all([p1, p2, p3]); // if p2 rejects, Promise.all rejects
// ✅ handle individually (settled results)
const results = await Promise.allSettled([p1, p2, p3]);
results.forEach(r => {
if (r.status === 'fulfilled') console.log('ok', r.value);
else console.warn('failed', r.reason);
});
Promise.allSettled or guard individual promises when you need fault tolerance.8. Final Thoughts
Callbacks were messy, Promises made async manageable, and async/await made it readable. None of them are enemies. Use promises for parallelism, async/await for clarity, and mix both when appropriate.
If you enjoyed this, follow me for daily dev insights on JavaScript, Python, and Cloud — and tell me: which async pattern do you use most?
Follow me on Medium
Comments
Post a Comment