React 16 Error Boundaries

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, there’s some good news this week!

React 16 was officially released on Sept 26th

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 that are cumbersome today (fragments and portals). 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 (the real problem occurs) and where you hit the ground (when 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 which may have been failing silently or non-catastrophically will now cause the entire app to unmount!

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 live inside and are scoped by component hierarchy instead of inside a given block of synchronous javascript.

Error boundaries exist because this ↓ 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 each others’ errors, 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 and risking the possible exposure of corrupted state or undefined behavior. It’s a big improvement for your application’s stability.

On React 16? Use error boundaries today!

Error boundaries are something that every large React application should make use of 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 them?

You’re allowed to do whatever you see fit in componentDidCatch, but after it’s been triggered, you cannot render this.props.children, and must replace them with some sort of fallback UI. This fallback might mean rendering a heartfelt apology, a debug view, a feedback form, a link to support, a funny gif — or 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
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 — meaning 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. For 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.

Send the errors to Sentry!

Because error boundaries are analogous to try/catch, exceptions they capture will not 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;
    }
  }
}

Play with a live demo of this code here and read (or fork!) the complete code on glitch.One more cool aspect of componentDidCatch is the second argument, errorInfo. Currently errorInfo has one property — componentStack — 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 in Sentry
componentStack as seen in Sentry

If you want to read more about Error Handling in React 16, the best place is this in-depth blogpost by the React Dev team.