reactjsexceptionundefinedundefined-referencereact-error-boundary

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


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?


Solution

  • 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:

    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.