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.