JavaScript Error Handling with Promises

JavaScript promises allow asynchronous code to use structured error handling. The promise chains are served as a great way of error handling.

Whenever a promise rejects, the control jumps to the nearest rejection handler. One of the most useful methods of error-handling is .catch.

catch

As it was already mentioned, .catch is one of the most useful methods for error handling in JavaScript.

Let’s view a case, in which the URL to fetch is wrong (no such site), and the .catch method handles it:

fetch('https://noSuchServer.someText') // rejects
  .then(response => response.json())
  .catch(err => console.log(err)) // TypeError: failed to fetch (the text may vary)

But note that .catch is not immediate. It might appear after one or several .then.

Everything might be correct with the site, but the response is not valid JSON. The simplest way of catching all the errors is to append .catch to the end of the chain. Here is an example:

fetch('/promiseChaining/user.json')
  .then(response => response.json())
  .then(user => fetch(`https://api.github.com/users/${user.name}`))
  .then(response => response.json())
  .then(user => new Promise((resolve, reject) => {
    let img = document.createElement('img');
    img.src = user.avatarUrl;
    img.className = "promiseAvatarExample";
    document.body.append(img);
    setTimeout(() => {
      img.remove();
      resolve(user);
    }, 3000);
  }))
  .catch(error => console.log(error.message));

Usually, .catch doesn’t trigger. But, in case any of the promises, as mentioned earlier, rejects, it will catch it.

Implicit try..catch

There is an invisible try..catch around the code of a promise handler and promise executor. In case of an exception, it will be treated as a rejection.

It is shown in the following code:

new Promise((resolve, reject) => {
  throw new Error("Error!!");
}).catch(console.log); // Error: Error!!

It operates the same as this code:

new Promise((resolve, reject) => {
    reject(new Error("Error!!"));
  }).catch(console.log); // Error: Error!!

The invisible try..catch will catch the error and transform it into a rejected promise. It can happen both in the executor function and its handlers. In case you throw inside a .then handler, it means a rejected promise, and the control jumps to the closest error handler.

An example will look like this:

new Promise((resolve, reject) => {
  resolve("Yes");
}).then((result) => {
  throw new Error("Error!!"); // rejects the promise
}).catch(console.log); // Error: Error!!

That may happen to all the errors, not just ones caused by the throw statement.

We can consider a programming error as an example:

new Promise((resolve, reject) => {
  resolve("Yes");
}).then((result) => {
  someFunction(); // no such function
}).catch(console.log); // ReferenceError: someFunction is not defined

The final .catch will catch both explicit rejections and accidental errors in the handlers.

Rethrowing

As it was already stated, .catch at the end of the promise chain is equivalent to try..catch. You may have as many .then handlers as you like, then use a single .catch at the end of the chain for handling all the errors.

The regulartry..catch allows you to analyze an error and rethrow it if it can’t be handled. A similar situation is possible for promises.

If you throw inside .catch, then the control will go to the next nearest error handler. If you handle the error and finish normally, it will continue to the next nearest successful .then handler.

The example below illustrates the successful error-handling with .catch:

// the execution: catch -> then
new Promise((resolve, reject) => {
  throw new Error("Error!!");
}).catch(function (error) {
  console.log("The error-handling, continue normally");
}).then(() => console.log("The next successful handler runs"));

In the example above, the .catch block finishes normally. Hence, the next effective .then is called.

Now let’s check out another situation with .catch. The handler (*) catches the error but is not capable of handling it:

// the execution: catch -> catch -> then
new Promise((resolve, reject) => {
  throw new Error("Error!!");
}).catch(function (error) { // (*)
  if (error instanceof URIError) {
    // handle
  } else {
    console.log("Can't handle such a error");
    throw error; // throwing this or that error jumps to the next catch
  }
}).then(function () {
  /* doesn't run here */
}).catch(error => { // (**)
  console.log(`The unknown error: ${error}`);
  // do not return anything => execution goes the usual way
});

The execution jumps from the initial .catch (*) to the following one down the chain.

Unhandled Rejections

In this section, we will examine the cases when errors are not handled.

Let’s see that you have forgotten to append .catch to the end of the chain.

Here is an example:

new Promise(function () {
    noSuchFunc(); // Error here, no such function
  })
  .then(() => {
    // successful promise handlers
  }); // no append .catch at the end

If there is an error, the promise will be rejected. The execution will jump to the nearest rejection handler. But there exists none, and the error will get “stuck”.There isn’t any code for handling it.

So, what will happen if an error is not caught by try..catch? The script will collapse with a console message. Things like that occur with unhandled promise rejections.

The engine of JavaScript usually tracks this kind of rejections, generating a global error.

For catching such errors in the browser, you can use the event unhandledRejection, as follows:

window.addEventListener('unhandledRejection', function (event) {
  // the event object has two special properties
  console.log(event.promise); // [object Promise] - error
  console.log(event.reason); // Error: Error!! - the unhandled error
});
new Promise(function () {
  throw new Error("Error!!");
}); // no catch to handle the error

So, in case there is an error, and no .catch can be found, the unhandledRejection will trigger getting the event object with the information regarding the error.

As a rule, this kind of errors are unrecoverable. The most proper solution in such circumstances is to inform the user about it, reporting the incident to the server.

Non-browser environments, such as Node.js, include other options for tracking unhandled errors.

Summary

One of the most significant assets of using promises is the way they allow you to handle errors.

Errors in the promises can be handled with .catch: no matter it’s a reject() call or an error thrown in a handler. It would be best if you put .catch precisely in the places where you want to handle errors. The handler analyzes the errors rethrowing the ones that are unknown (for example, programming mistakes).

In any other case, you need to have unhandledRejection event handler ( for browsers and analogs of different environments). It will track unhandled errors informing the user about them. It will help you avoid the collapse of your app.




Do you find this helpful?

Related articles