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.
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.
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.
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.
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.
Let's bring back componentDidCatch and explore it further. It receives two arguments that allow us to track and investigate the errors further.
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.
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.
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.
In the render method, make sure to render the fallback UI if an error occurred. Otherwise, we just render the component's children.
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.
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.
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.
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.
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.
We introduce a componentDidMount, and throw an error in there. As expected, the fallback UI is rendered.
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.
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.