Error Handling using Error Boundaries in React 16

Nik Graf
InstructorNik Graf

Share this video with your friends

Send Tweet
Published 5 years ago
Updated 3 years ago

A JavaScript error in the UI shouldn’t break the whole application. To solve this problem React 16 introduced the new concept of an error boundary.

Error boundaries are React components that catch JavaScript errors anywhere in their child component tree, log those errors, and display a fallback UI instead of the crashed component tree.

In this lesson we'll create an error boundary. We can use the new lifecycle hook componentDidCatch to handle any errors that the components we wrap throw at us, without unmounting our entire app!

[00:00] Here, you can see an application rendering a profile. The profile component expects that a user object with the property name is passed in to render the name. Further, render button to update the user. As you can see, this works perfectly fine.

[00:15] Unfortunately, it doesn't work if the profile receives null for the user, since it will try to access the property name on null, which then throws an Uncaught TypeError seeing that it cannot read property name of null.

[00:28] To handle such errors. React 16 introduces the life-cycle log, componentDidCatch. It allows us to catch JavaScript errors from anywhere in a component's child component tree. In order to inform the user that something went wrong, we're going to update the state, and then indicate that an error happened.

[00:51] In render, we had a check for the state and render fallback UI. When using React 16 or above, it's important, for a good user experience, to include this behavior since errors that were not caught will result in unmounting of the whole React component tree.

[01:17] Let me demonstrate this by commenting out componentDidCatch, refresh the page, and updating the user. As you can see, the content has been removed. The whole component tree is gone.

[01:27] This wasn't the case in React 15 and lower, as the UI stayed untouched there. This behavior changed as, based on experiences that the React team made, it holds the view that it is worse to leave corrupted UI in place rather than to completely remove it.

[01:44] Let's bring back componentDidCatch and explore it further. It receives two arguments that allow us to track and investigate the errors further.

[01:52] The first argument is the error instance itself. The second one is info containing the component stack. When running in production, this especially is useful to send to an error reporting service to identify where your application breaks down.

[02:21] In its current state, our app component became what the React team describes as an error boundary. It is defined by the following three traits.

[02:30] It catches JavaScript errors anywhere in the child component tree, logs these errors, and display fallback UI instead of the component tree when it crashes.

[02:40] As the next step, we want to extract the error boundary functionality to a separate component in order to separate the error handling into a reusable unit. We create a new component, myErrorBoundary. It includes the state hasError, as well as our componentDidCatch functionality.

[03:17] In the render method, make sure to render the fallback UI if an error occurred. Otherwise, we just render the component's children.

[03:31] We can now wrap our profile inside our error boundary, and as expected, this results in the same behavior, where we now have a reusable error boundary component.

[03:56] This allows us also to decide if, for example, the update button is in or outside the error boundary. Another example would be multiple profiles, each of them wrapped in their own error boundary.

[04:09] Since error boundaries work with deeply nested component structures, it's probably best to put it in a few strategic places, rather than every level. Keep in mind to have at least one error boundary in place since, as already mentioned, uncaught errors result in unmounting of the whole component tree.

[04:28] Last but not least, we want to explore which errors are caught. First of all, any error in a function component, but also in the render method of class component. We change our profile to a class component, and as you can see, the same effect.

[04:50] In addition to that, any error thrown in a constructor will also render the fallback UI. Further, any lifecycle method thrown in error will also be caught by a parent's componentDidCatch.

[05:12] We introduce a componentDidMount, and throw an error in there. As expected, the fallback UI is rendered.

[05:22] It's important to know that errors thrown in event handlers aren't caught by componentDidCatch, with one exception -- an error that is thrown inside a function passed to setState. Let me demonstrate this.

[05:36] First, we add a on-click handler throwing an error. As you can see, the error is not caught. Now, we add setState, and pass in a function throwing an error. As you can see, the error is caught, and the fallback UI is rendered.

Deniz Oguz
Deniz Oguz
~ 5 years ago

It is seems this video is starting from middle of something. I had to stop and rewind a few times to catch topic. But content is explained well. Thanks.

Gabor
Gabor
~ 5 years ago

This seems useful. Also nicely explained, thanks!

Vamshi
Vamshi
~ 5 years ago

Dumb question: Why do you do this.setState(state => {...state, hasError: true}) rather than this.setState({hasError: true}) ?

Vamshi
Vamshi
~ 5 years ago
wangshijun
wangshijun
~ 5 years ago

Great video!

jpbamberg1993
jpbamberg1993
~ 5 years ago

This is not working for me. I copied your code straight from the code pen example at the bottom. It renders the correct error message for a second the redirects to a regular error page. I created the app I am using to follow along with create-react-app. I would also like to second Vamshi's question on the ...state?

lucas
lucas
~ 5 years ago

@Nik Graf

Great lesson....

Your code editor, Seems really great, What's the editor you are using? and how did you config it? thanks~

Alan Plum
Alan Plum
~ 5 years ago

@jpbamberg1993 Try adding "type='button'" to the button. The default button type is "submit" which would cause the browser to submit the form. As the button isn't actually in a form this shouldn't do anything but your browser may disagree.

Mauro Carrero
Mauro Carrero
~ 5 years ago

This is not working for me. I copied your code straight from the code pen example at the bottom. It renders the correct error message for a second the redirects to a regular error page. I created the app I am using to follow along with create-react-app. I would also like to second Vamshi's question on the ...state?

It happens the same here, it seems that the problem is the development build, try building and running production build and it will work. It looks like the problem (or expected behaviour once you know about it) is to avoid breaking the app only in prod. If you see react-dom.development.js in the stack trace you will have the error in the screen, if you see react-dom.production.js you should not.

Regarding Vamshi's question, is preserving the state only adding the hasErrors flag.

Aman Gupta
Aman Gupta
~ 5 years ago

this.setState(state => { throw new Error("Oh nooo!"); return { ... state }; })

why return statement after throw error ? doesn't it break the execution at throw statement itself ?

Stephen
Stephen
~ 5 years ago

Where's the beginning of this, am I missing something??

Stephen
Stephen
~ 5 years ago

NIce explanation,

Melissa Clausse
Melissa Clausse
~ 4 years ago

Dumb question: Why do you do this.setState(state => {...state, hasError: true}) rather than this.setState({hasError: true}) ?

...state is called a spread operator. What it does here, is it passes the whole state object into setState, and only updates the part you want to update, hasError: true. If you did this.setState({hasError: true}), you would be replacing the entire state object with just your hasError: true line.

Read more about spread attributes in JSX over here: JSX Spread Attributes

Estefan Antelo
Estefan Antelo
~ 4 years ago

@Melissa there are no dumb questions. You are right, but I think the author intended to do just what he has done.

If you replace the state with a {hasError: true} object, you end up implicitly removing every other part of the state. The author is creating a new state based on the old but overwriting the hasError value. I think this is the most common way to update your component's state. Do you agree?

Spread Attributes make use of the Spread Operator as well, but are used in another context (passing props from an object to a subcomponent)

Here's the Specification for the Spread Operator: https://tc39.github.io/proposal-object-rest-spread/

I hope this helped you!

Kind Regards, Steve :)

Randy Creasi
Randy Creasi
~ 4 years ago

Dumb question: Why do you do this.setState(state => {...state, hasError: true}) rather than this.setState({hasError: true}) ?

...state is called a spread operator. What it does here, is it passes the whole state object into setState, and only updates the part you want to update, hasError: true. If you did this.setState({hasError: true}), you would be replacing the entire state object with just your hasError: true line.

Read more about spread attributes in JSX over here: JSX Spread Attributes

That's not correct. The object you pass to setState need only contain the state variables you want to update. The setState method does the merging of other properties for you. See https://reactjs.org/docs/react-component.html#setstate

Chetan Kantharia
Chetan Kantharia
~ 4 years ago

Do you guys has any recommendation for remote error reporting service?

Arno Lenin
Arno Lenin
~ 4 years ago

@Randy is correct,

Melissa Clausse
Melissa Clausse
~ 4 years ago

@Estefan, the "dumb question" part was a quote, I was replying to Vamshi's comment further up the thread :)

@Randy As stated in your link "Subsequent calls will override values from previous calls in the same cycle, so the quantity will only be incremented once. If the next state depends on the previous state, we recommend using the updater function form, instead". That's what I was trying to explain as the difference between

  • this.setState({hasError: true}) which is a shallow merge and overrides hasError regardless of any other calls to setState and
  • this.setState((state) => {...state, hasError: true}) where state is the previous state which ensures you're always making an update based on latest state.
Melissa Clausse
Melissa Clausse
~ 4 years ago

@Randy Sorry, I realize my original post doesn't express the above as I intended it to. Upon rereading my own block I realize it is incorrectly stating that the entire state would be overriden which is not what I meant to say. I meant to say it would be hard-overriding the state rather than taking into account previous state. My bad! Thanks for helping clarify.

Randy Creasi
Randy Creasi
~ 4 years ago

@Melissa That's a great explanation why you'd want to pass a function (instead of an object) to setState. However, even in that case it's not necessary to spread the previous state into the object that function returns. The previous state is made available to you only to enable you to calculate a new value based upon it (e.g. { counter: prevState.counter + 1 }) not because it's required (meaning { ...prevState, counter: prevState.counter + 1 } is equivalent and no more correct).

Melissa Clausse
Melissa Clausse
~ 4 years ago

@Randy Ah, I see, thank you very much - very helpful explanation. Do you know if passing a function is considered innefficient or if it can have any negative consequences? I've gotten into the habit of passing a function as shown above whenever manipulating the state but if it's unnecessary I'd prefer using the shorthand.

Randy Creasi
Randy Creasi
~ 4 years ago

@Melissa Any function call incurs a slight overhead so in a strict sense you could say it's less efficient. But, as always with optimization, it's best not to worry about that until it becomes noticeable and you've profiled your app to find the bottlenecks. In most cases it will never become an issue. At least that's what I've heard from people much smarter than me :) This inspires me to start passing a function more often!

Christian
Christian
~ 4 years ago

Any feedback from the author?!?!?