DEV Community

loading...
Cover image for Solving the mystery of Promise *catch* method - and learning more about the *then* on the way

Solving the mystery of Promise *catch* method - and learning more about the *then* on the way

mpodlasin profile image Mateusz Podlasin ・7 min read

catch is a well known method for handling errors in Promise code. It's easy to learn and simple to use.

But I have noticed that many programmers who only know Promises superficially, think that catch is the only way to handle errors in Promises code, or at least that it is always the preferable one.

And this is simply not true. I use catch very often and it definitely has its place, but in this article I want to prove to you that in order to handle errors in more subtle ways, you will sometimes need other means of dealing with exceptions.

This will teach us not only about the catch itself, but will also unveil interesting details about the then method!

So in this article we will begin by learning more about the then method first. Knowing it deeply will allow us to solve the "mystery" of catch - what it really is, how exactly does it work and when it should and shouldn't be used.

Let's begin!

Intricacies of then

In real world application, the most common way to use then method is to simply pass it one argument - a callback function:

somePromise.then(result => {
   // do something with `result`
   return newResult;
});
Enter fullscreen mode Exit fullscreen mode

When somePromise resolves (for example a HTTP request finishes), our callback passed to then gets called with a value to which somePromise have resolved (for example JSON that we received from that HTTP request).

We can do whatever we want with the result inside the callback and optionally we can return some newResult.

This pattern is bread and butter of programming with Promises and that's why people believe that is really all you can do with then.

But then (sic!) comes an awkward moment. We make a HTTP request and we want to handle possible errors from that request, but we also have the case where we want to throw an error, for example when validation of the incoming JSON fails:

httpRequest
    .then(jsonResult => {
        if (!isValid(jsonResult)) {
            throw new Error('This JSON is bad!');
        }
        // if JSON is valid, simply do something with it
    });
Enter fullscreen mode Exit fullscreen mode

On this snippet, if the JSON is invalid, we will throw an error, which will get propagated further. That's what we want.

But also if there are any errors coming directly from httpRequest, they will be propagated as well. This we don't want. We want to handle those errors, but only those.

So what would be the solution? Many programmers who know then, know also about catch method. So probably the first attempt would look something like this:

httpRequest
    .then(jsonResult => {
        if (!isValid(jsonResult)) {
            throw new Error('This JSON is bad!');
        }
        // if JSON is valid, simply do something with it
    })
    .catch(httpRequestError => {
        // handle somehow the HTTP request error
    });
Enter fullscreen mode Exit fullscreen mode

This however doesn't work as we want.

Yes, all the errors from httpRequest will be caught and handled, but also all the errors coming from our then callback, including validation error, will be caught as well!

And not only they will be caught, they will also be handled just like HTTP errors, because our catch callback is only prepared for those kinds of exceptions. This might in turn cause even more troubles in the error handling function and result in difficult to track bugs.

So the second thought might be to move catch method above the then method:

httpRequest
    .catch(httpRequestError => {
        // handle somehow the HTTP request error
    })
    .then(jsonResult => {
        if (!isValid(jsonResult)) {
            throw new Error('This JSON is bad!');
        }
        // if JSON is valid, simply do something with it
    });
Enter fullscreen mode Exit fullscreen mode

This is quite worrying solution, because at the beginning it will seem to work. If HTTP request resolves correctly, then method will be called as intended. If JSON validation fails, the error will be thrown and it will not be caught by any catch, just as we want.

However if the HTTP request fails, catch callback will be called. What will happen next is that the then method will be called right after!

If we don't return anything in our catch callback, the then callback will called with an undefined value:

httpRequest
    .catch(httpRequestError => {
        // we are handling an error, but not
        // returning anything there
    })
    .then(jsonResult => {
        // if `httpRequest` threw an error,
        // this callback will be called,
        // with `jsonResult` having value `undefined`
    });
Enter fullscreen mode Exit fullscreen mode

We might mitigate that, by simply bailing out from executing the then callback when its argument is undefined:

httpRequest
    .catch(httpRequestError => {
        // handle somehow the HTTP request error
    })
    .then(jsonResult => {
        if (!jsonResult) {
            return;
        }

        if (!isValid(jsonResult)) {
            throw new Error('This JSON is bad!');
        }
        // if JSON is valid, simply do something with it
    });
Enter fullscreen mode Exit fullscreen mode

This will work, but it's still kind of awkward and verbose. We simply don't want to call a callback handling JSON when we don't have a JSON to handle! So how would we do that?

That's exactly where the second argument to then comes with the help. The second argument of then method is also a callback, but it is an error handling callback. It will be called only when some Promise higher in the call chain throws an error which doesn't get caught and handled before.

So let's rewrite our example:

httpRequest
    .then(
        jsonResult => {
            if (!isValid(jsonResult)) {
                throw new Error('This JSON is bad!');
            }
            // if JSON is valid, simply do something with it
        },
        httpRequestError => {
            // handle somehow the HTTP request error
        }
    );
Enter fullscreen mode Exit fullscreen mode

It's cleaner, there is less code and we don't have to do any awkward undefined checks.

And, indeed, it works just as we want. The trick here is that the error handling function passed to then only reacts to errors that happen earlier in the call chain, not errors that happen in the thens first callback.

So in this example all the errors coming from httpRequest will be caught, but our validation error, happening in the callback, will not.

Furthermore, then will always call only one of the two callbacks. If everything goes right, it will simply call the first callback, as usual. If there is an unhandled exception higher in the chain, it will call only the second callback.

So we don't have to do any ifs in the first callback. If we don't get a proper JSON result from the httpRequest, the JSON handling function will simply never be called.

Nice, isn't it?

Default callbacks of then method

We are getting closer to solving the mystery of catch.

In order to finally answer that question, we need to take just a last, closer look at arguments that can be passed to the then method.

We already showed that then accepts a second, optional argument. But it might be a surprise to you that the first argument is... optional as well!

It turns out, that you can think of both the first and the second arguments of then as having default values, which will be used if you don't provide any function.

The default function for the first argument is:

result => result
Enter fullscreen mode Exit fullscreen mode

and the default function for the second argument is:

error => throw error;
Enter fullscreen mode Exit fullscreen mode

It means that if you don't provide the first argument to the then, the method will simply take the value from the previous Promise and pass it on further.

On the other hand, if the previous Promise throws an error, the default error handling function of then will simply rethrow that error.

These are very sensible default behaviors, behaving so intuitively, that sometimes programmers don't even think about their existence.

Solving the mystery of catch

With all this knowledge, we are at the moment when we can talk more about the catch method itself.

It is a method that is, as we said before, a go to method for error handling for the most of the JavaScript programmers.

But do those that use it really understand how it works? After all, it seems that then method has already built-in error handling. How in that case catch relates to then?

The thing you have to think about is how catch behaves when the Promise, to which it's attached to, doesn't throw an error:

const somePromiseWithCatch = Promise.resolve(5)
    .catch(error => console.log(error);
Enter fullscreen mode Exit fullscreen mode

If Promise.resolve(5) would throw an error, this error would be logged to the console.

But it doesn't - Promise.resolve(5) immediately resolves to a number 5. So what result we will get in the end? How the catch will behave here with a Promise that doesn't throw any errors?

Let's attach a then to this newly constructed Promise:

somePromiseWithCatch.then(value => console.log(value));
Enter fullscreen mode Exit fullscreen mode

As you surely expected, after running this code number 5 gets printed to the console. So what catch does, is that it simply returns the same value as the previous Promise, as long as that Promise didn't throw any error.

All this information now should be enough for you to solve the mystery of catch by yourself.

What is catch?

It's simply a then method without the first argument!!!

Indeed, the two following examples work in exactly the same way!

somePromise.catch(error => { 
    /* somehow handle the error */ 
});
Enter fullscreen mode Exit fullscreen mode
somePromise.then(undefined, error => { 
    /* somehow handle the error */ 
});
Enter fullscreen mode Exit fullscreen mode

Note how we passed undefined as a first argument to then so that it's default callback function is used.

We might have as well write:

somePromise.then(result => result, error => { 
    /* somehow handle the error */ 
});
Enter fullscreen mode Exit fullscreen mode

which would again result in the same behavior.

And if you still don't believe me that it can be that simple, just take a look at how catch is described in the EcmaScript Standard:

catch description in EcmaScript Standard

Conclusion

In this article we solved the "mystery" of catch. We showed that it is not a completely original method, but merely a tiny wrapper for a then method, which we could easily write ourselves.

Obviously it's so handy to use, that it was added to the native Promises, to make our programming cleaner. After all it's easier to catch the catch with your eye among the many lines of then calls.

But on the way we have seen that sometimes it is beneficial to use error handling built-in into then method directly, because it can give you more fine tuned control over which errors you want to handle and which ones you don't.

I hope this article gave you a deeper understanding of both catch and then methods.

If you enjoyed this article, considered following me on Twitter, where I am regularly posting articles on JavaScript programming.

Thanks for reading!

(Cover Photo by Keith Johnston on Unsplash)

Discussion (0)

Forem Open with the Forem app