Node.js Fundamentals
Course 1 · Chapter 3 · Callbacks & Promises
🔄 Callbacks & Promises
Asynchronous programming is core to Node.js. We've seen callbacks briefly; now we dive deeper into why they matter and their problems. We'll also introduce Promises — a cleaner way to handle async operations and avoid "callback hell." This chapter covers callback patterns, promise basics, and promise chains.
📞 Callbacks Revisited
A callback is a function passed to another function, called when an operation completes:
function fetchUser(id, callback) {
setTimeout(() => {
const user = { id, name: 'Alice' };
callback(null, user);
}, 1000);
}
fetchUser(1, (err, user) => {
if (err) {
console.error('Error:', err);
} else {
console.log('User:', user);
}
});
⚠️ Callback Hell (Pyramid of Doom)
When callbacks nest deeply, code becomes hard to read and maintain:
The Problem
getUser(1, (err, user) => {
if (err) {
console.error(err);
} else {
getOrders(user.id, (err, orders) => {
if (err) {
console.error(err);
} else {
getOrderDetails(orders[0].id, (err, details) => {
if (err) {
console.error(err);
} else {
console.log('Order details:', details);
}
});
}
});
}
});
Each nested operation adds another level of indentation. This is hard to debug and modify.
✨ Promises: A Better Way
A Promise represents a value that will be available in the future. It has three states:
Pending
Operation hasn't finished yet. Waiting for completion.
Resolved (Fulfilled)
Operation succeeded. Promise has a value.
Rejected
Operation failed. Promise has an error.
Settled
Either resolved or rejected. No longer pending.
Creating a Promise
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
const success = true;
if (success) {
resolve('Operation succeeded!');
} else {
reject(new Error('Operation failed!'));
}
}, 1000);
});
promise
.then(result => console.log(result))
.catch(error => console.error(error));
🔗 .then() and .catch()
.then() runs when the promise resolves. .catch() runs when it rejects.
function fetchData(id) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (id > 0) {
resolve({ id, data: 'Some data' });
} else {
reject(new Error('Invalid ID'));
}
}, 1000);
});
}
fetchData(1)
.then(result => console.log('Got:', result))
.catch(error => console.error('Error:', error.message));
fetchData(-1)
.then(result => console.log('Got:', result))
.catch(error => console.error('Error:', error.message));
⛓️ Promise Chains
Chain multiple operations. Each .then() passes its result to the next:
Callback Hell vs Promise Chains
Callbacks (Hard to Read)
getUser(1, (err, user) => {
if (err) {
console.error(err);
} else {
getOrders(user.id, (err, orders) => {
if (err) {
console.error(err);
} else {
console.log(orders);
}
});
}
});
Promises (Readable)
getUser(1)
.then(user => getOrders(user.id))
.then(orders => console.log(orders))
.catch(error => console.error(error));
How Promise Chains Work
fetchUser(1)
.then(user => {
console.log('User:', user);
return fetchOrders(user.id);
})
.then(orders => {
console.log('Orders:', orders);
return orders[0].id;
})
.then(orderId => {
console.log('First order ID:', orderId);
})
.catch(error => {
console.error('Error in chain:', error);
});
Key point: Each .then() receives the resolved value from the previous .then(). If you return a promise, the next .then() waits for it. If you return a value, it's automatically wrapped in a resolved promise.
🔀 Parallel Operations: Promise.all()
Run multiple promises in parallel and wait for all to complete:
const p1 = fetchUser(1);
const p2 = fetchUser(2);
const p3 = fetchUser(3);
Promise.all([p1, p2, p3])
.then(([user1, user2, user3]) => {
console.log('All users:', user1, user2, user3);
})
.catch(error => {
console.error('One of them failed:', error);
});
Note: If any promise rejects, Promise.all() rejects immediately.
⚡ First to Finish: Promise.race()
Waits for the first promise to settle (resolve or reject):
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout!')), 5000)
);
Promise.race([fetchData(), timeout])
.then(result => console.log('Got result:', result))
.catch(error => console.error('Error or timeout:', error.message));
💻 Coding Challenges
Challenge 1: Convert Callbacks to Promises
Take a callback-based function and convert it to return a promise. Then chain multiple calls using .then().
Goal: Practice promise creation and chaining.
→ Solution
Challenge 2: Promise.all() for Parallel Operations
Create three promise-returning functions that fetch different data. Use Promise.all() to fetch all three in parallel and log the combined results.
Goal: Understand parallel promise execution.
→ Solution
Challenge 3: Error Handling in Chains
Create a promise chain where one operation fails. Show how the error propagates down to .catch() and how to recover from errors mid-chain.
Goal: Master promise error handling.
→ Solution
💡 Promise vs Callback Cheat Sheet
Callbacks: Require error checking in every callback. Nest deeply for sequential operations. Hard to debug.
Promises: Error handling with a single .catch(). Cleaner chains. Better error propagation.
Next: async/await makes promises even cleaner — but understanding promises is the foundation!
🎯 What's Next
Promises are powerful, but we can make them even easier to read with async/await — JavaScript's modern async syntax. We'll explore that in the next chapter.