Promises Error Handling

Chapter 24 14 mins

Learning outcomes:

  1. What is error handling
  2. How to handle errors in JavaScript
  3. The onRejected() callback
  4. The catch() method

Introduction

One of the biggest concerns of complex applications, be they for web or desktop, is to handle errors effectively.

This requires the use of the conventional try..catch statements bundled with the throw keyword; in addition to listening for error events, laying out numerous if checks in the code and much more on this way.

Sometimes it also requires passing in callbacks to be fired on the occurrence of any errors. When this is the case and the overall code is quite involved, then using a callback simply means to be writing code that will ultimately lead to unmanageable clutter - as discussed in detail in the Promises Introduction chapter.

What's rather a better alternative is to use a promise!

In this chapter we shall introduce you to the catch() method inherited by all promises and see how it's analogous to the second argument to then(), and how to use all these features to handle bugs occurring in the wrapped-up asynchronous operation.

Let's begin!

What is error handling?

We'll start by answering the question - what is error handling - using our all-time favourite example i.e AJAX.

Suppose you make a request to some random file on your server using XMLHttpRequest() - the core of AJAX. What errors do you think can possibly occur in the whole request-response cycle?

Do this as a quick exercise - it will give you a good warmup on the topic!

To name a few:

  1. The request can be made to a non-existent file in which case the server would send a 404, Not Found, status code.
  2. The server script at the backend might have an invalid syntax in which case it would response with a 500 Internal Server Error, status.
  3. The client's network could be down in which case the error event would be dispatched.
  4. The request can violate the CORS policy in which case, once again, the error event will be fired.

and so on....

Actually, it depends on the application itself - for example it can be configured to parse a JSON response and then read a property on the parsed object to determine the status of the response.

Whatever the case be, the main point over here is that errors can happen in any asynchronous operation and thereby it's imperative for us to write code that handles them effectively.

How to handle errors? Well it's pretty elementary!

Error handling in JavaScript is usually done with two things: events and conditional statements.

Events such as error and abort frequently fire, even in simplistic applications; likewise it's common to provide onerror and onabort handlers to eventually respond to each case.

Similarly, often times one needs to lay out conditional checks in order target errors - for example by checking the status property of an XMLHttpRequest() object against the value 200 in the load event, we can throw an exception once we know it's not equal to 200.

As another example: we can check for the availability of the XMLHttpRequest() API and consequently throw an exception if it's not suported.

So now that we know how to handle errors in programming, it's finally time that we dive right into implementing it in promises.

Rejection callback

Recall the then() method and the callback arguments we provide to it: the first one is the onFulfilled callback which fires once the promise is fulfilled while the second one is the onRejected callback which fires once the promise is rejected.

If onRejected is provided; well and good, but if it's not, then the default "Thrower" argument is taken to be the callback.

The question is that when does a given promise get rejected!

Well there are two ways to reject a promise: one is by invoking the reject() callback passed to the executor whereas the other is by throwing an exception explicitly inside the executor, using the throw keyword.

The way the latter works is described as follows:

When the Promise() constructor is instantiated, it immediately executes the provided executor function, inside a try block - much like the code below:

function Promise(executor) {
    // invoke the executor function
    try { executor(); }
    catch(e) { reject(e); }
}

The corresponding catch block calls the same reject() function passed to the executor, and supplies it with the error argument e, as shown above.

This means that any thrown exceptions inside the executor will cause the corresponding promise to be rejected with the thrown value.

Let's consider a couple of examples.

Following is the code to illustrate promise rejection, done by explicitly calling the reject() argument:

var p = new Promise(function(resolve, reject) {
    setTimeout(function() {
        reject("Sorry");
    }, 3000);
});

Here if we log the promise object p after 3 seconds, we'll get something similar to the following:

Promise {<rejected>: "Sorry"}

If we want we can also pass in a callback to then() to handle the promise's rejection after 3 seconds. This is shown below:

p.then(null, function(error) {
    console.log("An error occurred: " + error);
});

The second argument here is an anonymous function that logs an error message in the console, when invoked roughly after 3 seconds.

An error occurred: Sorry
Recall that it's the second argument to then() that deals with errors; NOT the first one, which in this case is set to null (it isn't required for the example and so there's no point of giving one).

Now for the second case - throwing an exception explicitly within the promise - consider the code below:

var p = new Promise(function(resolve, reject) {
    setTimeout(function() {
        throw "Sorry";
    }, 3000);
});

Although it's different syntactically, this code does exactly the same thing as the one shown above with reject(). When the statement throw "Sorry" is executed, control flow shifts to the internal catch block, which then calls the reject() function with the throw value "Sorry".

Once again, we can attach a failure callback to the promise p here, which'll operate exactly the same as in the previous example.

p.then(null, function(error) {
    console.log("An error occurred: " + error);
});
An error occurred: Sorry

Moving on, as we know from the previous chapter on Promise Chaining, then() returns a promise which depends on the respective passed-in callback to complete.

In the occasion where it throws an error itself, the returned promise is rejected with the thrown value.

Consider the code below:

var p = new Promise(function(resolve, reject) {
    resolve("OK");
});

var p2 = p.then(function(data) {
    throw "Sorry";
});

console.log(p2);
Promise {<rejected>: "Sorry"}

See how the main promise p is resolved but the one derived by calling then() i.e p2, is rejected - simply because the callback throws an error.

A derived promise is rejected with value v, if an exception v is thrown in the corresponding then()'s callback.

On the same lines, we can also return a promise in the callback that gets rejected eventually and thus causes the derived promise to get rejected as well:

var p = new Promise(function(resolve, reject) {
    resolve("OK");
});

var p2 = p.then(function(data) {
    // return a rejected promise
    return new Promise(function(resolve, reject) {
        reject("Sorry");
    });
});

If we log p2 after a while over here, we'll get something similar to the following:

Promise {<rejected>: "Sorry"}

This happens because when a then() callback returns a promise itself, the corresponding derived promise mimics that returned promise - in this case p2 mimics the promise in line 7.

Now retreating back to the scenario where an explicit exception is thrown inside the callback for then(), we have an amazing concept following from this very basic idea, which we're about to explore next.

First of all it's vital for us to understand that if we don't provide a failure callback to then(), the method will default it to the "Thrower" function - which will simply throw an exception with the value of the promise.

Consider the code below:

var p = new Promise(function(resolve, reject) {
    reject("Oops!");
});

p.then(null, function(error) {
    throw error;
});

This is the same as writing the following (with the second argument to then() omitted this time):

var p = new Promise(function(resolve, reject) {
    reject("Oops!");
});

p.then(null);
For more info on the default arguments to then(), please read Promises Basics.

With this understood, try to solve the task below and see how well have you learnt promises overall!

What log will be made in the console by the following code? Explain your answer.

Conventional catching

The method then() both works and sounds well in putting up a fulfillment callback on a promise object.

However when it comes to the failure callback, it doesn't look that meaningful, especially in cases where multiple async operations are to be handled by a single callback.

For instance, consider the code below:

var p = new Promise(function(resolve, reject) {
    someAsyncOperation();
}).
then(function(data) {
    someOtherAsyncOperation();
}).
then(function(data) {
    someOtherAsyncOperation2();
}).
then(null, function(error) {
    alert("An error occurred: " + error);
});

Notice how the last then() method serves the job of handling any errors occurring in the upstream chain of promises, but doesn't right away seem to do so.

Does the last then() call, in the code above, anyhow seem to be handling errors? Does it visually convey that message to you?

The reason why this happens is that we're not fairly used to identifier names like then to serve the purpose of catching any thrown exceptions. This purpose is well served by the conventional name 'catch'.

Moreover, if you notice closely you'll see that when our task is to only handle errors, then() regardless requires us to pass in two arguments - one could be null or undefined while the second has to be error-handling function.

In other words, then() enforces us to mention at least two arguments even in error-handling cases.

Hence, what all this led to was the team of developers of ES6 giving a method catch() to promises. Let's discuss on it!

For a given function f(),

catch(f) is exactly synonymous with then(null, f).

The method catch() is nothing new; just a spoon of syntactic sugar over the powerful then() method!

Following we demonstrate a couple of examples using this method.

Here we are using catch() to handle a very basic promise rejection.

var p = new Promise(function(resolve, reject) {
    setTimeout(function() {
        reject("Sorry");
    }, 3000);
});

p.catch(function(error) {
    console.log("An error occurred: " + error);
});
catch() only accepts one argument i.e the onRejected() callback to fire on the main promise's rejection.

In contrast, below we use the method in a much more useful way - the one illustrated at the start of this section i.e handling errors in a chain of promises:

var p = new Promise(function(resolve, reject) {
    someAsyncOperation();
}).
then(function(data) {
    someOtherAsyncOperation();
}).
then(function(data) {
    someOtherAsyncOperation2();
}).
catch(function(error) {
    alert("An error occurred: " + error);
});

See how the last catch() call only requires us to pass in one argument i.e the error-handling callback, as opposed to passing an additional null value to then().

In conclusion

At this point you shall be well versed with a couple of things such as how catch() works internally, how it can handle errors occurring anywhere in an entire promise chain and so on.

You shall also appreciate the fact that it's not the magic of catch() to be able to error-handle chains of promises; instead it's the magic of the "Thrower" function that throws errors and thus gets them to travel downstream into the catch() method!

In short, error handling is crucial to programming and similarly crucial to promises in JavaScript. While callbacks work well syntactically in dealing with simple errors, as soon as the underlying asynchronous operation becomes more complicated they start to lose this essence.

The best solution for complex use cases is, therefore, to use promises instead and the techniques for handling errors inside them.