Learning Objectives

  • Master the .then() method
  • Learn proper error handling with .catch()
  • Understand cleanup with .finally()
  • Chain Promise methods effectively

The .then() Method

.then() is used to handle a fulfilled Promise:

promise.then(onFulfilled, onRejected);

Basic Usage

const promise = Promise.resolve('Hello');

promise.then(value => {
  console.log(value); // 'Hello'
});

.then() Always Returns a Promise

const promise1 = Promise.resolve(1);
const promise2 = promise1.then(x => x + 1);
const promise3 = promise2.then(x => x + 1);

promise3.then(result => console.log(result)); // 3

Returning Values in .then()

1. Return a Value

The next .then() receives that value:

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

2. Return a Promise

The next .then() waits for that Promise:

Promise.resolve(1)
  .then(x => {
    return new Promise(resolve => {
      setTimeout(() => resolve(x * 2), 1000);
    });
  })
  .then(x => console.log(x)); // 2 (after 1 second)

3. Return Nothing (undefined)

Promise.resolve(5)
  .then(x => {
    console.log(x); // 5
    // No return statement
  })
  .then(x => console.log(x)); // undefined

The .catch() Method

.catch() handles rejected Promises:

promise.catch(onRejected);

// Equivalent to:
promise.then(null, onRejected);

Basic Error Handling

Promise.reject(new Error('Failed'))
  .catch(error => {
    console.error('Caught:', error.message);
  });

.catch() in a Chain

fetchUser(1)
  .then(user => fetchOrders(user.id))
  .then(orders => processOrders(orders))
  .catch(error => {
    console.error('Error anywhere in chain:', error);
  });

Important: .catch() catches errors from any previous step in the chain.

Error Propagation

Errors propagate down the chain until caught:

Promise.resolve(1)
  .then(x => {
    throw new Error('Oops!');
  })
  .then(x => {
    console.log('This will not run');
  })
  .then(x => {
    console.log('This will not run either');
  })
  .catch(error => {
    console.error('Caught here:', error.message);
  });

The .finally() Method

.finally() runs regardless of success or failure:

promise.finally(onFinally);

Use Cases

Perfect for cleanup operations:

showLoadingSpinner();

fetchData()
  .then(data => displayData(data))
  .catch(error => showError(error))
  .finally(() => {
    hideLoadingSpinner(); // Always runs
  });

Key Characteristics

  1. No arguments: .finally() callback receives no arguments
  2. Transparent: Passes through the previous value/error
  3. Always runs: Executes on both success and failure
Promise.resolve('value')
  .finally(() => {
    console.log('Cleanup');
    // Cannot access 'value' here
  })
  .then(value => {
    console.log(value); // 'value' - passed through
  });

Chaining .then(), .catch(), and .finally()

Pattern 1: Standard Chain

doSomething()
  .then(result => doSomethingElse(result))
  .then(newResult => doThirdThing(newResult))
  .catch(error => console.error(error))
  .finally(() => cleanup());

Pattern 2: Recovering from Errors

.catch() can return a value to recover:

fetchPrimaryAPI()
  .catch(error => {
    console.log('Primary failed, trying backup');
    return fetchBackupAPI();
  })
  .then(data => {
    console.log('Got data:', data);
  });

Real-World Example: API Call with Loading State

let isLoading = false;

function fetchUserData(userId) {
  isLoading = true;
  
  return fetch(`/api/users/${userId}`)
    .then(response => {
      if (!response.ok) {
        throw new Error('Network response was not ok');
      }
      return response.json();
    })
    .then(data => {
      console.log('User data:', data);
      return data;
    })
    .catch(error => {
      console.error('Failed to fetch user:', error);
      throw error; // Re-throw to propagate
    })
    .finally(() => {
      isLoading = false;
    });
}

Common Mistakes

❌ Forgetting to Return

// Bad - breaks the chain
promise
  .then(result => {
    doSomething(result); // Not returned!
  })
  .then(result => {
    console.log(result); // undefined
  });

// Good
promise
  .then(result => {
    return doSomething(result);
  })
  .then(result => {
    console.log(result); // Correct value
  });

❌ Nesting Instead of Chaining

// Bad - callback hell with Promises
promise.then(result => {
  doSomething(result).then(newResult => {
    doMore(newResult).then(finalResult => {
      console.log(finalResult);
    });
  });
});

// Good - flat chain
promise
  .then(result => doSomething(result))
  .then(newResult => doMore(newResult))
  .then(finalResult => console.log(finalResult));

❌ Not Handling Errors

// Bad - unhandled rejection
promise.then(result => {
  console.log(result);
});

// Good - always handle errors
promise
  .then(result => {
    console.log(result);
  })
  .catch(error => {
    console.error(error);
  });

Key Takeaways

  • .then() handles fulfilled Promises and returns a new Promise
  • .catch() handles rejected Promises from any previous step
  • .finally() runs cleanup code regardless of outcome
  • ✅ Always return values in .then() to maintain the chain
  • ✅ Prefer flat chains over nested Promises
  • ✅ Always handle errors with .catch()

Next Steps

Next, we'll learn about Promise.all() and how to run multiple Promises in parallel!