JavaScript Promise error handling with catch and finally

Let's see how to handle errors in JavaScript Promises. You might think you can put a try/catch around a Promise:

try {
  const p = new Promise((resolve, reject) => {
    console.log("I think I'm about to throw up!");
    throw new Error('boom');
  });

  p.then(() => console.log("I'm never called."));
} catch (e) {
  console.log("I'm never called too!");
}

When you run the code above, the error won't be caught and the script will crash. The try/catch (in this case) is not able to catch the Promise error. But you can catch it like this:

const p = new Promise((resolve, reject) => {
  console.log("I think I'm about to throw up!");
  throw new Error('boom');
});

p.catch((e) => console.log(e.message));

There is a catch method chained to the promise p. And this catch catches errors that happen in the Promise chain before the catch. For instance:

new Promise((resolve, reject) => {
  throw new Error('boom');
})
  .then(() => console.log('I never run.'))
  .catch((e) => console.log(e.message)); // It shows boom

It's recommended to have a catch at the end of all promise chains.

Sometimes you might want to always execute something after a catch or a then. For that you use a finally:

new Promise((resolve, reject) => {
  resolve('all good');
})
  .then((v) => console.log(v)) // Logs "all good"
  .catch((e) => console.log(e))
  .finally(() => console.log('finally!')); // Logs "finally!"

If the previous promise was rejected, it would still work. You can put finally anywhere in the promise chain:

new Promise((resolve, reject) => resolve('all good'))
  .then((v) => console.log(v)) // Logs "all good"
  .finally(() => console.log('finally!')) // Logs "finally!"
  .catch((e) => console.log(e))
  // Logs "finally again!"
  .finally(() => console.log('finally again!'));

Error handling gotcha: reject vs catch

By now you might be thinking that the reject handler is the same as catch:

// A, the catch catches the error
new Promise((resolve, reject) => reject('boom'))
  .then(null)
  .catch((e) => console.log(e)); // Logs boom

// B, the `then` method handles the
// error (notice the second argument of the then function)
new Promise((resolve, reject) => {
  reject('boom');
}).then(null, (e) => console.log(e)); // Logs boom

In this case it is exactly the same. But what happens if the resolve function throws an error?

const badOnResolve = () => {
  throw new Error('boom on resolve');
};

// A, the catch catches the error
new Promise((resolve, reject) => resolve('boom'))
  .then(badOnResolve)
  .catch((e) => console.log(e.message)); // Logs "boom on resolve"

// B, the `then` method handles the error
// (notice the second argument)
new Promise((resolve, reject) => {
  resolve('looks good here');
}).then(badOnResolve, (e) => console.log('onReject'));
// Doesn't log "onReject", it throws an uncaught error.

It doesn't catch the error! If the resolve has an issue, only a catch or the next onReject can catch the error. The next A and B are similar:

const badOnResolve = () => {
  throw new Error('boom on resolve');
};

// A
new Promise((resolve, reject) => {
  resolve('looks good here');
})
  .then(badOnResolve)
  .then(null, (e) => console.log(e.message));

// B
new Promise((resolve, reject) => {
  resolve('looks good here');
})
  .then(badOnResolve)
  .catch((e) => console.log(e.message));

Errors inside any handler on the then method can only be treated on the next reject handler or the next catch.

The following diagram may help: Error on resolve function

And in any case, remember that

catch(func) is the same as then(undefined, func)

Reject Promise vs Throw Promise

You might think rejecting or throwing inside a Promise is the same:

new Promise((resolve, reject) => {
  reject(new Error('boom'));
}).catch((e) => console.log(e.message)); // boom

new Promise((resolve, reject) => {
  throw new Error('boom');
}).catch((e) => console.log(e.message)); // boom

This is all fine, but if you throw an error inside an asynchronous call inside the Promise function, it won't work:

new Promise(() => {
  // An asynchronous callback.
  setTimeout(() => {
    throw new Error('boom');
  }, 0);
}).catch((e) => console.log("I don't show!"));

The result is an uncaught error. If you use reject, you won't have this problem:

new Promise((resolve, reject) => {
  // An asynchronous callback.
  setTimeout(() => reject(new Error('boom')), 0);
}).catch((e) => console.log(e.message)); // boom

Error on reject

We've been passing an error to the reject function. Why don't we just pass a message like we do for the resolve? The reason is because an error contains information about the stack trace:

new Promise((resolve, reject) => {
  reject(new Error('boom'));
}).catch((e) => console.log(e.message, e.stack));
// Shows "boom" and the stack trace.

It's recommended to always send an error on the reject to have extra information about the error.

Questions

What's the output?

const p = new Promise((resolve, reject) => {
  throw new Error('boom');
});

p.catch((e) => console.log(e.message));

What's the output?

const logErr = (e) => console.log(e.message);

new Promise((resolve, reject) => {
  reject(new Error('boom'));
}).catch(logErr);

new Promise((resolve, reject) => {
  throw new Error('boom');
}).catch(logErr);

What's the output?

const log = (e) => console.log(e.message);

new Promise((resolve, reject) =>
  setTimeout(() => reject(new Error('boom')), 0)
).catch(log);

What's the output?

const log = (e) => console.log(e.message);

new Promise(() =>
  setTimeout(() => {
    throw new Error('boom');
  }, 0)
).catch(log);

What's the output?

Promise.resolve('ok')
  .then(console.log)
  .then(() => {
    throw new Error('boom');
  })
  .then(console.log)
  .catch((e) => console.log(e.message));

What's the output?

Promise.resolve('ok1')
  .then(() => {
    throw new Error('nok');
  })
  .catch((e) => console.log(e.message))
  .then(() => console.log('ok2'));

What's the output?

const p = new Promise((resolve, reject) => {
  reject('boom');
});

p.catch(console.log);

What's the output?

const p = new Promise((resolve, reject) => {
  throw new Error('boom');
  reject(new Error('baam'));
});

p.catch((e) => console.log(e.message));

What's the output?

new Promise((resolve, reject) => {
  throw new Error('boom');
})
  .then(() => console.log('hi'))
  .catch((e) => console.log(e.message));

What's the output?

new Promise((resolve) => {
  resolve('resolve');
})
  .then(console.log)
  .then(console.log)
  .catch(console.log)
  .finally(() => console.log('fin'));

What's the output?

new Promise((resolve) => resolve('ok'))
  .then((v) => console.log(v))
  .finally(() => console.log('fin1'))
  .catch((e) => console.log(e))
  .finally(() => console.log('fin2'));

What's the output?

const p = new Promise((_, reject) => reject("rej"))
p.then(null).catch(console.log);
p.then(null, (e) => console.log(e));

What's the output?

const f = () => {
  throw new Error('boom');
};

const p = new Promise((res) => res('ok'));
const logErr = (e) => console.log(e.message);

p.then(f).catch(logErr);
p.then(f, logErr);

What's the output?

const f = () => {
  throw new Error('boom');
};

const p = new Promise((res) => res('ok'));
const logErr = (e) => console.log(e.message);

p.then(f).then(null, logErr);
p.then(f).catch(logErr);