Catch Errors in a JavaScript Promise Chain with Promise.prototype.catch()

Marius Schulz
InstructorMarius Schulz

Share this video with your friends

Send Tweet
Published 4 years ago
Updated 2 years ago

The Promise.prototype.then() method accepts two callbacks as parameters, onFulfilled and onRejected:

  • If the promise is fulfilled, onFulfilled will be called.
  • If the promise is rejected, onRejected will be called.
  • If the promise never settles (that is, stays pending forever), neither one will be called.

If you want to register a handler for rejected promises only, you can use the Promise.prototype.catch() method: .catch(onRejected) behaves the same as .then(undefined, onRejected).

Instructor: [00:00] Here's the code that we ended up with in the previous lesson. We fetch a bunch of data from the Star Wars API and then display it in an output div. Let's now see what happens if we're trying to fetch data from a domain that does not exist.

[00:13] I'm going to add a hyphen here. I'm going to head over to the browser, open the dev tools, and refresh the page. As you can see, we now get a JavaScript error because the fetch API cannot connect to that host.

[00:26] As a result, the promise that is returned by the fetch call is rejected, and our two fulfillment handlers are never called. In addition to the network error that we saw in the first line, we also get another error in the second line, that says "Uncaught in promise."

[00:42] This is because we didn't register any rejection handlers in our promise chain. We only registered two fulfillment handlers.

[00:49] Let's take a step back and talk about the fulfillment and rejection of a promise. When we call the .then method on a promise, we can specify two handlers. The first one is the onFulfilled handler. The onFulfilled handler receives the value that the promise is fulfilled with as a parameter. In our case, that is the response.

[01:11] In addition to the onFulfilled handler, we can also specify an onRejected handler. The onRejected handler is called with the reason for why the promise was rejected. You can stick with reason. I usually call the parameter "error." Let's use console.warn in this case just to have a little bit of color in the console.

[01:30] If we now head over to the browser and refresh the page, we're going to see that we have a "TypeError -- Failed to fetch" logged to the console. In this case, let's write a user-friendly error message to the output, just so we know what's happening.

[01:49] If we pass both an onFulfilled and an onRejected handler to the then method, only one of them is ever going to be called, but never both. It's always one or the other. Let's go ahead and properly implement the onFulfilled handler.

[02:04] First, we need to fix the subdomain up here. Then we're going to come back here. What we want to do is we want to read the response body as JSON. We're going to use the response.json method again. Once that is done, we have our films.

[02:20] After that, we can set the innerText of the output to the result of getFilmTitles. There's an important thing we're missing here. That is the return keyword. response.json returns us a promise. If we keep chaining to it, we will still have a promise.

[02:38] That promise, we want to return from our fulfillment handler. Otherwise, we have a dangling promise chain. There's no way for us to get hold of it later if we want to attach handlers to it. Let's make sure this works. Yup, that's looking good.

[02:56] So far, we've been looking at what happens when fetch returns a rejected promise. What happens if, for example, response.json returns a rejected promise. We can simulate that the JSON parsing failed by using the Promise.reject method.

[03:11] We're going to be passing to it a string like "Invalid JSON." If we now refresh the page, we're going to see that we're going to get an uncaught error again. This is because we do not have an onRejected handler for this promise.

[03:25] Remember, either the onFulfilled handler is going to be executed, or the onRejected handler, but never both. We could solve this by adding another onRejected handler here, but there's another thing we can do.

[03:40] We can take our existing error handler, cut it out here, and append another .then call here. This time, we're not going to be passing an onFulfilled handler. We're just going to be passing our onRejected handler. Give this a save and refresh again.

[04:00] Now you can see that we're triggering our onRejected handler. This approach works because Promise.reject will give us a rejected promise. Because we don't have an onRejected handler here, this promise is also rejected. We're returning it from this fulfillment handler. This is what finally triggers the onRejected down here.

[04:21] This approach is so common that we have another method for it. It's called catch. If we have a rejected promise anywhere in our promise chain, we're going to fall through until we hit the first onRejected handler. The beauty of this approach is that we will also catch errors that result from invalid domain names here.

[04:39] I'm going to add back the hyphen we had before. I'm going to refresh the page. We're still going to be triggering our error handling logic. Let me go back and fix both the domain up here and the JSON parsing down here because there's one more stumbling block I want to show you.

[04:58] What happens when we're trying to access an endpoint that doesn't exist? Let's find out. We get an HTTP 404 error. Then we get an error that says that films.sort is not a function. Apparently, films is not an array anymore. Let's got to the network tab and find out why.

[05:19] Here we can see our failed network request. If we dig into it, we can see that the API returns an empty object when the endpoint can't be found. My question is "Why was our fulfillment handler called?" Shouldn't the promise have been rejected? No. This is not how the fetch API works.

[05:38] Even if we got back a non-successful HTTP status code, the promise is still resolved because we did get back a response. If we want the promise to be rejected if the HTTP status code is unsuccessful, we can implement that ourselves. We can check if the response was not OK and in that case throw an error.

[06:00] Any error thrown within our onFulfillment or onRejected handler is going to be turned into a rejected promise. That rejected promise is going to be caught by our onRejected handler down here. Let's make sure this is actually the case. Yup, that's looking good because now we can see our error handling logic down here.

[06:20] Finally, let's also make sure that our success case is still working. We're going to say, "Films" here. Refresh one last time. Here you go. This is how you can do error handling with promises.

~ a month ago

Thanks for creating this course Maruis.

At about 2:44 the importance of returning from the Promise was mentioned. During times when it is not required to chain on the promise further can one choose not to return, or, this could lead to some unintended consequences?

Marius Schulz
Marius Schulzinstructor
~ 3 weeks ago

@Nikhil: In those cases where there’s no further promise chaining, you can decide not to return any value. I would still highly encourage you to always end your promise chain with an error handler, e.g. using a promiseDone() util function.

~ 3 weeks ago

Hmm, ok @Marius. Had looked for ways to end the promise chain, including looking for promiseDone() usages, however, did not find anything yet. Will continue to look further, in the meanwhile if possible, sharing a code sample on how this is done would be great.