Callbacks & Promises

Node.js Fundamentals — Callbacks & Promises
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) { // Simulate async operation (like database query) setTimeout(() => { const user = { id, name: 'Alice' }; callback(null, user); }, 1000); } // Use it 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) => { // Async operation setTimeout(() => { const success = true; if (success) { resolve('Operation succeeded!'); // Fulfilled } else { reject(new Error('Operation failed!')); // Rejected } }, 1000); }); promise .then(result => console.log(result)) // Runs if resolved .catch(error => console.error(error)); // Runs if rejected

🔗 .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); }); } // Success case fetchData(1) .then(result => console.log('Got:', result)) .catch(error => console.error('Error:', error.message)); // Error case 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); // Returns a promise }) .then(orders => { console.log('Orders:', orders); return orders[0].id; // Return a value (auto-wrapped in promise) }) .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.