JavaScript Error Handling


Example

Errors thrown from promises are handled by the second parameter (reject) passed to then or by the handler passed to catch:

throwErrorAsync()
  .then(null, error => { /* handle error here */ });
// or
throwErrorAsync()
  .catch(error => { /* handle error here */ });

Chaining

If you have a promise chain then an error will cause resolve handlers to be skipped:

throwErrorAsync()
  .then(() => { /* never called */ })
  .catch(error => { /* handle error here */ });

The same applies to your then functions. If a resolve handler throws an exception then the next reject handler will be invoked:

doSomethingAsync()
  .then(result => { throwErrorSync(); })
  .then(() => { /* never called */ })
  .catch(error => { /* handle error from throwErrorSync() */ });

An error handler returns a new promise, allowing you to continue a promise chain. The promise returned by the error handler is resolved with the value returned by the handler:

throwErrorAsync()
  .catch(error => { /* handle error here */; return result; })
  .then(result => { /* handle result here */ });

You can let an error cascade down a promise chain by re-throwing the error:

throwErrorAsync()
  .catch(error => {
      /* handle error from throwErrorAsync() */
      throw error;
  })
  .then(() => { /* will not be called if there's an error */ })
  .catch(error => { /* will get called with the same error */ });

It is possible to throw an exception that is not handled by the promise by wrapping the throw statement inside a setTimeout callback:

new Promise((resolve, reject) => {
  setTimeout(() => { throw new Error(); });
});

This works because promises cannot handle exceptions thrown asynchronously.

Unhandled rejections

An error will be silently ignored if a promise doesn't have a catch block or reject handler:

throwErrorAsync()
  .then(() => { /* will not be called */ });
// error silently ignored

To prevent this, always use a catch block:

throwErrorAsync()
  .then(() => { /* will not be called */ })
  .catch(error => { /* handle error*/ });
// or
throwErrorAsync()
  .then(() => { /* will not be called */ }, error => { /* handle error*/ });

Alternatively, subscribe to the unhandledrejection event to catch any unhandled rejected promises:

window.addEventListener('unhandledrejection', event => {});

Some promises can handle their rejection later than their creation time. The rejectionhandled event gets fired whenever such a promise is handled:

window.addEventListener('unhandledrejection', event => console.log('unhandled'));
window.addEventListener('rejectionhandled', event => console.log('handled'));
var p = Promise.reject('test');

setTimeout(() => p.catch(console.log), 1000);

// Will print 'unhandled', and after one second 'test' and 'handled'

The event argument contains information about the rejection. event.reason is the error object and event.promise is the promise object that caused the event.

In Nodejs the rejectionhandled and unhandledrejection events are called rejectionHandled and unhandledRejection on process, respectively, and have a different signature:

process.on('rejectionHandled', (reason, promise) => {});
process.on('unhandledRejection', (reason, promise) => {});

The reason argument is the error object and the promise argument is a reference to the promise object that caused the event to fire.

Usage of these unhandledrejection and rejectionhandled events should be considered for debugging purposes only. Typically, all promises should handle their rejections.

Note: Currently, only Chrome 49+ and Node.js support unhandledrejection and rejectionhandled events.

Caveats

Chaining with fulfill and reject

The then(fulfill, reject) function (with both parameters not null) has unique and complex behavior, and shouldn't be used unless you know exactly how it works.

The function works as expected if given null for one of the inputs:

// the following calls are equivalent
promise.then(fulfill, null) 
promise.then(fulfill)

// the following calls are also equivalent
promise.then(null, reject) 
promise.catch(reject)

However, it adopts unique behavior when both inputs are given:

// the following calls are not equivalent!
promise.then(fulfill, reject)
promise.then(fulfill).catch(reject)

// the following calls are not equivalent!
promise.then(fulfill, reject)
promise.catch(reject).then(fulfill)

The then(fulfill, reject) function looks like it is a shortcut for then(fulfill).catch(reject), but it is not, and will cause problems if used interchangeably. One such problem is that the reject handler does not handle errors from the fulfill handler. Here is what will happen:

Promise.resolve() // previous promise is fulfilled
    .then(() => { throw new Error(); }, // error in the fulfill handler
        error => { /* this is not called! */ });

The above code will result in a rejected promise because the error is propagated. Compare it to the following code, which results in a fulfilled promise:

Promise.resolve() // previous promise is fulfilled
    .then(() => { throw new Error(); }) // error in the fulfill handler
    .catch(error => { /* handle error */ });

A similar problem exists when using then(fulfill, reject) interchangeably with catch(reject).then(fulfill), except with propagating fulfilled promises instead of rejected promises.

Synchronously throwing from function that should return a promise

Imagine a function like this:

function foo(arg) {
  if (arg === 'unexepectedValue') {
    throw new Error('UnexpectedValue')
  }

  return new Promise(resolve => 
    setTimeout(() => resolve(arg), 1000)
  )
}

If such function is used in the middle of a promise chain, then apparently there is no problem:

makeSomethingAsync().
  .then(() => foo('unexpectedValue'))
  .catch(err => console.log(err)) // <-- Error: UnexpectedValue will be caught here

However, if the same function is called outside of a promise chain, then the error will not be handled by it and will be thrown to the application:

foo('unexpectedValue') // <-- error will be thrown, so the application will crash
  .then(makeSomethingAsync) // <-- will not run
  .catch(err => console.log(err)) // <-- will not catch

There are 2 possible workarounds:

Return a rejected promise with the error

Instead of throwing, do as follows:

function foo(arg) {
  if (arg === 'unexepectedValue') {
    return Promise.reject(new Error('UnexpectedValue'))
  }

  return new Promise(resolve => 
    setTimeout(() => resolve(arg), 1000)
  )
}

Wrap your function into a promise chain

Your throw statement will be properly caught when it is already inside a promise chain:

function foo(arg) {
  return Promise.resolve()
    .then(() => {
      if (arg === 'unexepectedValue') {
        throw new Error('UnexpectedValue')
      }

      return new Promise(resolve => 
        setTimeout(() => resolve(arg), 1000)
      )
    })
}