React ErrorBoundary doesn't catch "cannot read properties of undefined"

Viewed 431

I faced a weird edge case with React ErrorBoundary. Exceptions are not being processed if they happen in inline expression. For example:

const page = webtexts[courseId].pages[pageFamilyId]; // Returns undefined
...
<ErrorBoundary>
    // "Cannot read properties of undefined (reading 'page_name')" won't be processed in ErrorBoundary
    <span>{page.page_name}</span>
</ErrorBoundary>

But if I wrap a problematic place in the component, it works properly.

const page = webtexts[courseId].pages[pageFamilyId]; // Returns undefined
...
const WrapFailingPage: React.FC<{ page: any }> = ({ page }) => {
    return <span>{page.page_name}</span>;
};
...
<ErrorBoundary>
    <WrapFailingPage page={page} /> // The error is properly processed in ErrorBoundary
</ErrorBoundary>

For the demo, I made a simple Codepen - https://codepen.io/OlexanderD/pen/wvpdpxY.

Should the ErrorBoundary behave in that way? Why the inline expressions can be problematic?

2 Answers

TL;DR

ErrorBoundary tracks only the code parts that were created with React.createElement(React.Component / React.[V]FC). Therefore, if you return a broken JSX from a custom React component, ErrorBoundary will pick it up and process.

Details:

Examples are made for the attached Codepen https://codepen.io/OlexanderD/pen/wvpdpxY

A bit of terminology:

  • element - any HTML tag. It'll be processed as React.createElement('div', ...)
  • component - any React component, not native HTML tag. I'll be processed as React.createElement(MyComponent, ...)

According to the docs, ErrorBoundary can process exceptions only in their child component tree, but I tried to catch an exception from the element.

React.createElement() can receives 2 types of arguments: tag name (div, span, ...) or React component. Therefore, when we simply use <div>{fail.absurdIntentionalError}</div> and pass it to parent div the React will generate this code:

React.createElement(
  "div",
  null,
  React.createElement("button", ...),
  showFailingChild &&
  React.createElement("div", null, fail.absurdIntentionalError)
);  

As you can see we're passing an element as the part of the props.children for the parent div. Therefore the ErrorBoundary won't process it as the part of the component tree.

But if we use some wrapper like FailingComponent, the generated JSX will look like this:

React.createElement(
  "div",
  null,
  React.createElement("button", ...),
  showFailingComponent &&
  React.createElement(FailingComponent, null)
);  

Now we're passing the component to the props.children for the parent div. And the ErrorBoundary will process the result of the FailingComponent as the part of the component tree.

It happens sometimes when you have not initiated Bugsnag correctly.
Here is an example that triggers an error:

    import Bugsnag from '@bugsnag/js';

    const bugsnagClient = Bugsnag.start({
    apiKey: 'my-bugsnag-api-key',
    // plugins: [new BugsnagPluginReact()], <----- assume not there
   releaseStage: 'production',
   });

    const ErrorBoundary = 
    Bugsnag.getPlugin('react').createErrorBoundary(React)

    <ErrorBoundary FallbackComponent={ErrorView}>
    <App store={store} />
    </ErrorBoundary>

The above will trigger the "React ErrorBoundary doesn't catch "cannot read properties of undefined" error.
Therefore, in this case including that code will fix the issue as below:

    import Bugsnag from '@bugsnag/js';
    import BugsnagPluginReact from '@bugsnag/plugin-react'; // <----- inserted here

    const bugsnagClient = Bugsnag.start({
    apiKey: 'my-bugsnag-api-key',
    plugins: [new BugsnagPluginReact()], // <----- inserted here
   releaseStage: 'production',
   });
Related