The difference between .then(fn, fn) and .then(fn).catch(fn) in JavaScript

Aug 20, 2024

In JavaScript, there are two ways to pass an error handler callback function to a promise:

Option 1 (.then(thenFn).catch(catchFn)):

async function getUserEmail(userId: string): Promise<string> {
  return getUserById(userId)
    .then((user) => user.email.toLowerCase())
    .catch((err) => {
      logError(err);
      return '';
    });
}

Option 2 (.then(thenFn, catchFn)):

async function getUserEmail(userId: string): Promise<string> {
  return getUserById(userId).then(
    (user) => user.email.toLowerCase(),
    (err) => {
      logError(err);
      return '';
    }
  );
}

What's The Difference?

On the surface, they look like they accomplish the same thing, but there is a subtle difference between the two.

Let's assume that in both examples above, getUserById will throw an error if no user is found. And in that scenario, we're ok with getUserEmail returning the empty string.

But let's get tricky and also assume that user.email can be null, which means user.email.toLowerCase() will throw an error in that scenario (because we're trying to call .toLowerCase() on null).

If user.email is null, Option 1 will catch the null.toLowerCase() error because .catch comes after .then in the promise chain. But in Option 2, the error will not be handled by the 2nd function passed to .then. Instead, getUserEmail will return a rejected promise, and it will either be handled higher up in the call chain or possibly not handled at all.

The way that I like to visualize this is that a .catch function will handle any errors that originated previous to it in the promise chain. But when you use the .then(thenFn, catchFn) syntax, the catchFn is at the same level as .thenFn in the promise chain. They are essentially two different paths the promise chain can go down; One is not before the other.

This distinction can lead to very tricky bugs if you often use the .then(thenFn, catchFn) syntax. But that syntax exists because there are use cases where you only want to catch an error from the previous promise chain, and you don't want catchFn to be called if thenFn throws an error or returns a rejected promise.