Learning Objectives

  • Master Promise chaining techniques
  • Understand how to return values in .then()
  • Learn to return Promises in .then()
  • Build multi-step async workflows

What is Promise Chaining?

Promise chaining allows you to execute multiple asynchronous operations in sequence, where each operation starts when the previous one succeeds.

fetchUser(1)
  .then(user => fetchPosts(user.id))
  .then(posts => fetchComments(posts[0].id))
  .then(comments => console.log(comments))
  .catch(error => console.error(error));

How Chaining Works

Every .then() returns a new Promise, allowing you to chain multiple operations:

const promise1 = fetchUser(1);
const promise2 = promise1.then(user => user.name);
const promise3 = promise2.then(name => name.toUpperCase());

promise3.then(result => console.log(result)); // "JOHN DOE"

Returning Values in .then()

1. Return a Regular Value

The value becomes the resolved value of the returned Promise:

Promise.resolve(5)
  .then(x => x * 2)        // Returns 10
  .then(x => x + 3)        // Returns 13
  .then(x => x.toString()) // Returns "13"
  .then(result => console.log(result)); // "13"

2. Return a Promise

The next .then() waits for that Promise to resolve:

fetchUser(1)
  .then(user => {
    console.log('Got user:', user.name);
    return fetchPosts(user.id); // Returns a Promise
  })
  .then(posts => {
    console.log('Got posts:', posts.length);
    return posts;
  });

3. Return Nothing (undefined)

The next .then() receives undefined:

Promise.resolve('data')
  .then(data => {
    console.log(data);
    // No return statement
  })
  .then(result => {
    console.log(result); // undefined
  });

Multi-Step API Calls

Example: User Profile with Posts and Comments

function getUserProfile(userId) {
  return fetchUser(userId)
    .then(user => {
      console.log('Fetched user:', user.name);
      return fetchPosts(user.id);
    })
    .then(posts => {
      console.log('Fetched posts:', posts.length);
      return fetchComments(posts[0].id);
    })
    .then(comments => {
      console.log('Fetched comments:', comments.length);
      return comments;
    })
    .catch(error => {
      console.error('Error in chain:', error);
      throw error;
    });
}

getUserProfile(1)
  .then(comments => console.log('Final result:', comments));

Passing Data Through the Chain

Problem: Losing Previous Values

// ❌ Lost access to user
fetchUser(1)
  .then(user => fetchPosts(user.id))
  .then(posts => {
    // Can't access user here!
    return posts;
  });

Solution 1: Nested Objects

fetchUser(1)
  .then(user => {
    return fetchPosts(user.id)
      .then(posts => ({ user, posts }));
  })
  .then(({ user, posts }) => {
    console.log(user.name, 'has', posts.length, 'posts');
  });

Solution 2: Promise.all()

fetchUser(1)
  .then(user => {
    return Promise.all([
      user,
      fetchPosts(user.id)
    ]);
  })
  .then(([user, posts]) => {
    console.log(user.name, 'has', posts.length, 'posts');
  });

Real-World Example: E-commerce Checkout

function processCheckout(userId, cartId) {
  let order;
  
  return validateCart(cartId)
    .then(cart => {
      console.log('Cart validated');
      return calculateTotal(cart);
    })
    .then(total => {
      console.log('Total calculated:', total);
      return processPayment(userId, total);
    })
    .then(payment => {
      console.log('Payment processed:', payment.id);
      return createOrder(userId, cartId, payment.id);
    })
    .then(createdOrder => {
      order = createdOrder;
      return sendConfirmationEmail(order);
    })
    .then(() => {
      console.log('Email sent');
      return updateInventory(order.items);
    })
    .then(() => {
      console.log('Inventory updated');
      return order;
    })
    .catch(error => {
      console.error('Checkout failed:', error);
      // Rollback logic here
      throw error;
    });
}

Chaining Best Practices

1. Keep Chains Flat

// ❌ Bad: Nested
promise.then(result => {
  doSomething(result).then(newResult => {
    doMore(newResult).then(finalResult => {
      console.log(finalResult);
    });
  });
});

// ✅ Good: Flat
promise
  .then(result => doSomething(result))
  .then(newResult => doMore(newResult))
  .then(finalResult => console.log(finalResult));

2. Always Return

// ❌ Bad: Forgot to return
promise
  .then(result => {
    doSomething(result); // Not returned!
  })
  .then(result => {
    console.log(result); // undefined
  });

// ✅ Good: Return the Promise
promise
  .then(result => {
    return doSomething(result);
  })
  .then(result => {
    console.log(result); // Correct value
  });

3. Handle Errors at the End

promise
  .then(step1)
  .then(step2)
  .then(step3)
  .catch(error => {
    // Catches errors from any step
    console.error('Error:', error);
  });

Transforming Data in Chains

fetchUser(1)
  .then(user => user.name)
  .then(name => name.toUpperCase())
  .then(upperName => `Hello, ${upperName}!`)
  .then(greeting => {
    console.log(greeting); // "Hello, JOHN DOE!"
  });

Conditional Chaining

function getUserData(userId, includeDetails = false) {
  return fetchUser(userId)
    .then(user => {
      if (includeDetails) {
        return fetchUserDetails(user.id)
          .then(details => ({ ...user, details }));
      }
      return user;
    })
    .then(userData => {
      console.log('User data:', userData);
      return userData;
    });
}

Key Takeaways

  • .then() always returns a new Promise
  • ✅ Return values to pass data to the next .then()
  • ✅ Return Promises to chain async operations
  • ✅ Keep chains flat, avoid nesting
  • ✅ Always return from .then() handlers
  • ✅ Use .catch() at the end to handle all errors

Next Steps

Next, we'll dive deeper into error handling patterns in Promise chains!