Monday, October 7

How to use Error Boundaries in React 16

Have you seen these in your console?

Cannot read property ‘getHostNode’ of null ?
TypeError: Cannot read property ‘_currentElement’ of null ?

If you maintain a React application, you know that this class of error messages can be among the most frustrating and difficult to resolve. Typical symptoms include stack traces that thread through dark regions of the React internals, devoid of hope or references to even a single line of your own code:

TypeError: Cannot read property 'getHostNode' of null ?
  at getHostNode(~/react/lib/ReactReconciler.js:64:0)
  at getHostNode(~/react/lib/ReactCompositeComponent.js:383:0) ?
  at getHostNode(~/react/lib/ReactReconciler.js:64:0)
  at getHostNode(~/react/lib/ReactChildReconciler.js:114:0)?
  at updateChildren(~/react/lib/ReactMultiChild.js:215:0)
  at _reconcilerUpdateChildren(~/react/lib/ReactMultiChild.js:314:0)
  at _updateChildren(~/react/lib/ReactMultiChild.js:301:0)
  at updateChildren(~/react/lib/ReactDOMComponent.js:942:0)
  at _updateDOMChildren(~/react/lib/ReactDOMComponent.js:760:0) ?
  at updateComponent(~/react/lib/ReactDOMComponent.js:718:0)
  at receiveComponent(~/react/lib/ReactReconciler.js:126:0)
  at receiveComponent(~/react/lib/ReactCompositeComponent.js:751:0) ?
  at _updateRenderedComponent(~/react/lib/ReactCompositeComponent.js:721:0)
  at _performComponentUpdate(~/react/lib/ReactCompositeComponent.js:642:0)
  at updateComponent(~/react/lib/ReactCompositeComponent.js:544:0)
  at receiveComponent(~/react/lib/ReactReconciler.js:126:0) ?
  at receiveComponent(~/react/lib/ReactCompositeComponent.js:751:0)
  at _updateRenderedComponent(~/react/lib/ReactCompositeComponent.js:721:0)
  at _performComponentUpdate(~/react/lib/ReactCompositeComponent.js:642:0)
  at updateComponent(~/react/lib/ReactCompositeComponent.js:544:0)
  at receiveComponent(~/react/lib/ReactReconciler.js:126:0)  ?
  at receiveComponent(~/react/lib/ReactCompositeComponent.js:751:0)
  at _updateRenderedComponent(~/react/lib/ReactCompositeComponent.js:721:0)
  at _performComponentUpdate(~/react/lib/ReactCompositeComponent.js:642:0)
  at updateComponent(~/react/lib/ReactCompositeComponent.js:544:0) ?
  at receiveComponent(~/react/lib/ReactReconciler.js:126:0)
  at receiveComponent(~/react/lib/ReactCompositeComponent.js:751:0) ?
  at _updateRenderedComponent(~/react/lib/ReactCompositeComponent.js:721:0)
  at _performComponentUpdate(~/react/lib/ReactCompositeComponent.js:642:0)
  at updateComponent(~/react/lib/ReactCompositeComponent.js:558:0)
  at performUpdateIfNecessary(~/react/lib/ReactReconciler.js:158:0)
  at performUpdateIfNecessary(~/react/lib/ReactUpdates.js:151:0) ?
  at call(~/react/lib/Transaction.js:138:0)
  at call(~/react/lib/Transaction.js:138:0)
  at call(~/react/lib/ReactUpdates.js:90:0)
  at perform(~/react/lib/ReactUpdates.js:173:0)
  at call(~/react/lib/Transaction.js:204:0)
  at closeAll(~/react/lib/Transaction.js:151:0)
  at perform(~/react/lib/ReactDefaultBatchingStrategy.js:63:0) ?
  at batchedUpdates(~/react/lib/ReactUpdates.js:98:0)
  at batchedUpdates(~/react/lib/ReactEventListener.js:150:0)

If this type of stack trace from a React error is a familiar sight to you, I’ve got some good news for you!

Say Hello to React 16

React 16 was officially released on September 26, 2017. React 16 is an API-compatible, under-the-hood rewrite of the React internals with ambitious goals like enabling asynchronous or preemptive rendering and providing new tools (with sweet names) for elegantly expressing component hierarchies, like fragments and portals, that are cumbersome today. Another goal of the architecture is handling errors with a new, more correct and rigorous strategy.

This strategy means React 16 prevents situations where an error inside of a render call causes an invalid state and results in undefined behavior or confusing errors (like our good friend Cannot read property 'getHostNode' of null) by unmounting the entire component tree when errors are thrown inside render or lifecycle methods.

Failing more aggressively on these errors means not allowing the application to continue running in a corrupted state — reducing the distance between where you trip (where the real problem occurs) and where you hit the ground (where React blows up because its internal bookkeeping is corrupt). This makes certain errors in React components easier to understand and fix.

However, it also means that existing problems that may have been failing silently or non-catastrophically will now cause the entire app to unmount. Yikes!

The solution to this is React 16’s new tool for explicitly handling error propagation, error boundaries. Error boundaries are analogous to try{ }catch(e){ } statements, but they live inside and are scoped by component hierarchy instead of inside a given block of synchronous JavaScript.

Error boundaries exist because ↓↓↓ doesn’t work with the JSX rendering model:

<div>
  { try {
    <CoolComplicatedComponent/>
  } catch(error){
    handleChildError(error)
    }
  }
</div>

By using error boundaries, you can isolate sections of your app’s component tree from errors in other sections, for instance, allowing your media player to continue running smoothly when the comment section crashes. This is without sacrificing React 16’s more predictable behavior or risking the possible exposure of corrupted state or undefined behavior. It’s a big improvement for your application’s stability.

Start Using Error Boundaries

Error boundaries are something that every large React application should use to improve error resiliency. The API surface of error boundaries is simple: any React component becomes an error boundary when it implements the new lifecycle method, componentDidCatch(error, info). This will be called with any uncaught error that bubbles up from the component’s children’s lifecycle methods, constructors, or render methods (but not the component’s own errors).

<div>
  <ExampleBoundary>
    <h2>Sidebar</h2>
    <Widget/>
  </ExampleBoundary>
  <p> This content won't unmount when Widget throws. </p>
</div>

<ExampleBoundary> is a React component that implements componentDidCatch (there’s an example further below).

What to do when you catch errors?

You’re allowed to do whatever you see fit in componentDidCatch. But after it has been triggered, you can’t render this.props.children, and you must replace it with some sort of fallback UI. This fallback might mean rendering a heartfelt apology, a debug view, a feedback form, a link to support, or a funny GIF. It could also mean just discreetly rendering nothing at all — it depends on the needs of your application. Just do yourself a favor and try to ensure it’s not something that will throw its own errors!

example fallback UI

Where should Error Boundaries go?

Error boundaries are an open-ended tool, and the community is still defining best practices for them. Our advice is to wrap components at the granularity they can be independently useful. This means that when one errors, the others can still accomplish their purpose. Wrapping your page content will protect your header or sidebar navigation components from being unmounted on an error and will give the user an easy way to back out and return to working parts of your application. By contrast, going to the extreme and wrapping every individual input component of a form could leave someone in a UI state that still responds to their input but can’t actually be used to finish their goal.

Note: by default, raven.js, Sentry’s JavaScript SDK, carefully instruments built-in methods to try to automatically wrap your code in try/catch blocks. It does this to attempt to capture error messages and stack traces from all your scripts, regardless of which origin they’re served from.

Send the errors to Sentry!

Because error boundaries are analogous to try/catch, exceptions they capture will not automatically be bubbled up to Sentry’s global uncaught error handler in production. Make sure to call Raven.captureException(error) in your componentDidCatch implementations to get insights into these errors and fix them.

Here’s an example of an error boundary component that apologizes and sends a report to Sentry plus gives the user the option to fill out a report of what happened:

class ExampleBoundary extends Component {
  constructor(props) {
    super(props);
    this.state = { error: null };
  }
  componentDidCatch(error, errorInfo) {
    this.setState({ error });
    Raven.captureException(error, { extra: errorInfo });
  }
  render() {
    if (this.state.error) {
      return (
        <div
          className="snap"
          onClick={() => Raven.lastEventId() && Raven.showReportDialog()}
        >
          <img src={oops} />
          <p>We're sorry — something's gone wrong.</p>
          <p>Our team has been notified, but click here fill out a report.</p>
        </div>
      );
    } else {
      //when there's not an error, render children untouched
      return this.props.children;
    }
  }
}

You can read (or fork!) the complete code here.

One more cool aspect of componentDidCatch is the second argument, errorInfo. Currently, errorInfo has one property, componentStack, which is a component stack trace. That’s the path through your component tree from your application root all the way to the offending component — something that helps map the error location to your mental model and codebase. Because this information is helpful for triaging and resolving errors, we recommend sending it with Sentry errors as extra data.

componentStack as seen in Sentry

As always, you can try Sentry for your React app (it’s 100% open source, too).

That’s it! Happy error monitoring.


Source: Scotch.io

0 0 votes
Article Rating
Subscribe
Notify of
guest

0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x