react: Error boundaries: Recover from errors thrown in `render`

So I’m trying to put some graceful error handling in case 1 of my views crap out:

var MyGoodView = React.createClass({
  render: function () {
    return <p>Cool</p>;
  }
});

var MyBadView = React.createClass({
  render: function () {
    throw new Error('crap');
  }
});

try {
  React.render(<MyBadView/>, document.body);
} catch (e) {
  React.render(<MyGoodView/>, document.body);
}

However, MyGoodView does not get rendered w/ the following stack trace:

stack trace

Seems like error throw React in a bad state where renderedComponent is undefined, thus cannot be unmounted. How do I handle this scenario?

About this issue

  • Original URL
  • State: closed
  • Created 10 years ago
  • Reactions: 91
  • Comments: 85 (17 by maintainers)

Commits related to this issue

Most upvoted comments

Catching errors in child components is now supported. You can use the new componentDidCatch() hook in React 16 Beta 1 which you can try now.

You can read more about this feature in our new blog post: Error Handling in React 16.

I think we can close this issue. It is likely the feature will evolve over time, and we’d love your feedback, but let’s discuss it in more focused future issues.

No. We settled on componentDidCatch.

There are quite a few places where you can’t throw exceptions without React ending up in an invalid state. If you can’t trust your developers then the only solution I know of is to put a try/catch in each render.

Small update on this: latest alphas of React (react@next and react-dom@next) are still not ready for wide use (there’s known issues with infinite loops in development), but they implement error boundaries so you can start playing with them (and see if they satisfy your use case).

The recommended way to use them is to create a component like this:

class ErrorBoundary extends React.Component {
  state = { error: null };

  unstable_handleError(error, info) {
    this.setState({ error });
    // Note: you can send error and info to analytics if you want too
  }

  render() {
    if (this.state.error) {
      // You can render anything you want here,
      // but note that if you try to render the original children
      // and they throw again, the error boundary will no longer catch that.
      return <h1>Something went wrong: {this.state.error.toString()}.</h1>;
    }
    return this.props.children;
  }
}

Then you can put it anywhere:

<ErrorBoundary>
  <MyApp />
</ErrorBoundary>

or to protect specific components:

<Layout
  left={<Sidebar />}
  right={<ErrorBoundary><RouteHandler /></ErrorBoundary>}
/>

Note that ErrorBoundary acts as a “catch” block. It only “catches” errors in its children, but not in itself. You can nest error boundaries (just like you can nest catch blocks).

The exact API may change in 16.

@DanielSundberg Yes, basic/unstable support was added for initial render in v15, and support for updates is being added in a future release. It will be documented when it becomes stable.

Just wrote elegant (and global) workaround by monkeypatching React.createElement. Check out this gist if you are looking for a solution before this issue makes it to a release: https://gist.github.com/Aldredcz/4d63b0a9049b00f54439f8780be7f0d8 EDIT: also supporting Hot reload

+1 It’s very scary that one child component error can take an entire component tree down. It would be awesome to put try/catch statements at some key components in that tree.

+1 This makes debugging React apps much more difficult than it has to be!

In our project we use the following as a base class for all components, outputs an error box if rendering fails. Ugly but works here:

class BaseComponent extends React.Component {
    constructor(props) {
        super(props);
    }

    _safeRender() {
        throw new Error('_safeRender must be implemented in derived components')
    }

    render() {
        try {
            return this._safeRender();
        } catch (error) {
            log.error(error.stack);

            return (
                <div className="error-message">
                    <p>{error.message}</p>
                    {error.stack}
                </div>
            );
        }
    }
}

@davidfurlong react-transform-catch-errors is not only deprecated but also doesn’t work with function components. AFAIK there is no better way than monkey-patch React.createElement. It may sound scary but it works reliable and doesn’t have any overhead.

Check out my library https://github.com/kossnocorp/react-guard. It’s used in several production apps for a few months already.

As a nice bonus, it allows to capture the component props & state:

An excerpt from a real-life bug captured with Sentry

I am working on an isomorphic app with many components, and we lean on very messy third-party APIs. So there is a relatively high risk that a developer can write a new component that makes bad assumptions about the data it receives (since we render server side, this destroys the request and the user cannot get any part of the page)

We decided to take an aggressive approach to the problem by wrapping React.createElement in our app and replacing the lifecycle methods with our own error handling. For ES6 we were thinking of doing the same thing by extending the Component class

Even though this leaves the app in an invalid state we thought it was preferable to rendering nothing for the user.

Here is a link to the code I am working on: https://github.com/skiano/react-safe-render/blob/feature/safe-methods/index.js

Is there some reason why I really should not do this?

Don’t know yet 😄 You can follow along with the remaining work on #8854 if you’re interested.

Some of the things we’re working on now/next include:

  • Smaller, more optimized renderer bundles.
  • A release of react-native that can use fiber. Getting ready to enter a couple week period of internal testing (at Facebook) for that renderer.
  • Server-side rendering. We now have some nice failing unit tests for it thanks to Sasha. (See isfiberreadyyet.com)
  • Some shallow renderer work remains as well that I think Leland and co are working on. (See airbnb/enzyme/pull/742, #8982)

Andrew and I are working toward one last 15.x release (probably next week) that adds a few deprecation notices and such for things that will be going away or changing in 16.

In the meanwhile there’s an alpha release available (16.0.0-alpha.6) if you’d like to play with the react-dom renderer and fiber.

ErrorBoundary seems interesting, however, how it would allow rescuing individual components instead of a (sub)tree? In the examples above when a small icon throws an error during render, the whole app/page won’t be rendered at all.

I’m imagining something like that:

ReactDOM.safeRender(
  <App />,
  document.getElementById('root'),
  (err, componentInstance) => {
    // If component fails then report the incident and render nothing
    reportError(err, componentInstance)
    return null
  }
)

…or at least on a class level, so that we could use inheritance.

class SafeComponent extends Component {
  catchRender (err) {
    reportError(err, this)
    return null
  }
}

class Whatever extends SafeComponent {
  render () {
    throw 'whoops'
    return <div>A small cosmetic detail</div>
  }
}

I’ve not voted on it myself, but I wouldn’t use it since it changes which method all the derived classes need to override to render, so special-case knowledge about how error handling in the base class works propagates to affect all React components. The current workaround I’m using is to wrap the render method in the base class in the constructor by calling trapRenderExceptions, which is very hacky but requires no changes to how the derived classes need to be written, so at least constrains the impact of the hack:

function trapRenderExceptions(component) {
    var oldRender = component.render;
    component.render = () => {
        try {
            return oldRender.call(component);
        }
        catch (e) {
            console.error("Serious error: Exception thrown in render. Report as a bug.");
            console.error(e);
            return null;
        }
    };
}

Very much looking forward to v16 with proper error boundaries!

Is unstable_handleError going to be the official name of the method in React 16?

I ended up implementing ErrorBoundary as a higher order component to wrap my components with.

export default function ErrorBoundary(WrappedComponent) {
  class ErrorBoundaryComponent extends React.Component {
    state = { error: null };

    unstable_handleError(error, info) {
      console.error(error, info);
      this.setState({ error });
      // Note: you can send error and info to analytics if you want too
    }

    render() {
      if (this.state.error) {
        return null;
      }
      return <WrappedComponent {...this.props} />;
    }
  };
  return ErrorBoundaryComponent;
}

This solution is considerably better than extending a base component because it can wrap functional components

function FunctionalComponent(props) {
  ...
}
export default ErrorBoundary(FunctionalComponent);

and catch errors that occur in the component’s other wrappers, such as a selector in redux’s mapStateToProps.

export default ErrorBoundary(connect(mapStateToProps, mapDispatchToProps)(SomeComponent));

@stevendesu fiber also gives you this info in a nicely formatted console warning: image

(In this example I threw an Error with the string “something has gone wrong”. Real errors would be more meaningful.)

Understood. Thanks for the example. Unfortunately I can’t seem to get it working. I have the unstable_handleError defined in a root component with a console log statement but I don’t see it called. Strange.

There is no support for doing this at individual component level (randomly disappearing individual components isn’t a great user experience). You are, however, free to wrap all your components with an error boundary, or to create a base class (although we generally don’t recommend that).

In the examples above when a small icon throws an error during render, the whole app/page won’t be rendered at all.

Why a whole app/page? You can divide your page into individual sections and wrap each of those with error boundary. The granularity is up to you.

We’ve used the BatchingStrategy API as a way to wrap try/catches around React code. It catches errors in render()s (initial and updates) and catches errors in your event handlers/callbacks. It doesn’t require any monkey-patching and doesn’t require you to wrap or modify any of your existing components.

HOWEVER, the BatchingStrategy API is going to be deprecated, so this strategy won’t work with React 16. We’ve used it successfully in React 0.13 through 15.5, and I think it’s a useful solution until error boundaries are ready.

More details here: https://engineering.classdojo.com/blog/2016/12/10/catching-react-errors/

That’s all fine and dandy, but I’m really just interested in seeing the error as an initial step. Currently the stack trace doesn’t even hint at where the code broke. You just know that “something rendered wrong”.

@stevendesu the Fiber implementation provides the component stack for the error (reference)

@kossnocorp react-guard Looks awesome and simple! Exactly what I was looking for! Thanks.

Any plan / possibility the unstable_handleError approach could be implemented to work in ReactDOM.renderToString? Currently React errors on var checkpoint = transaction.checkpoint(); in performInitialMountWithErrorHandling

It catches for the whole subtree (but not for the component itself).

👍 I look forward to exploring this a bit more, hopefully soon

Error boundaries don’t have to be at the root, as I mentioned before. You can put them in a few strategic places (e.g. route handler wrapper, and important components big enough to show a useful box).

I don’t think it’s a good idea to show them in place of every individual components. Components can be offscreen, or too little to show the box. In fact in DEV I’d probably always want to see a fullscreen error.

We still haven’t decided what’s the default way to show error boxes in React 16. @bvaughn was prototyping this at some point. We’ll get back to it before the release.

@jedwards1211

That’s the biggest difficulty it causes me. I sort of wish React would could just stop dead and not even throw any more errors, since it would be easier for me to find the root cause, and it’s not really in a viable state anyway.

Yes, and this is exactly what we’re doing in React 16. 😉

react-hot-loader v3 doesn’t seem to manage to wrap all of my functional stateless components… it would be really helpful if there were a supported way to guarantee a dev tool (i.e. in dev mode if not prod) would show a redbox if any rendering throws an error.

Please read my comment above: https://github.com/facebook/react/issues/2461#issuecomment-311077975. This is pretty much what we’ve added.

@joshclowater What is the benefit as opposed to using a regular component?

<ErrorBoundary>
  <Something />
</ErrorBoundary>

This gives you more freedom with granularity.

Whats the best practice for handling uncaught runtime errors in react lifecycle methods then? One uncaught error in production breaks the entire react component tree / serves a blank page. There must be a better solution than to add try/catch statements everywhere.

The package https://github.com/gaearon/react-transform-catch-errors seems like a good idea, but its deprecated & recommended not to be used in production.

Cheers

It seems as of React 15.0.2 the default behavior for performInitialMountWithErrorHandling is to swallow errors without any output. This makes debugging incredibly difficult. I don’t know if it still works this way with more recent versions, but I would suggest a default of spitting out a warning at least. Anything that would tell the developer what just happened. Thanks!

FYI, as a workaround I implemented this in my top level component:

  unstable_handleError(...args) {
    console.error(...args)
  }

Not a terrible inconvenience, but how do you find this issue thread when you don’t have an error to go off of? I had to run chrome with “pause on caught exceptions” to find it.

For those who need some kind of error handling in render functions in React 13/14 here is the workaround. Just take the following Interceptor.jsx:

/**
 * Interceptor function that returns JSX-component in case of error.
 * Usage example:
 * render: interceptor('MyClass.myFunc', function() {
 *   ... Here is some code that potentially can generate Exception
 * }, <b>Hallelujah! Another soul saved!</b>)
 *
 * @param where - Name of the function - will be displayed in console when exception is caught
 * @param renderFunction - Function where exception can be raised.
 * @param component - Optional component which should be displayed in case of error.
 * @returns {Function}
 */
var Interceptor = function (where, renderFunction, component) {
    return function () {
        if (typeof Config != 'undefined' && Config.General && Config.General.stackTraceEnabled) {
            if (console) {
                if (console.debug) {
                    console.debug(where);
                }
                else {
                    console.log(where);
                }
            }
        }
        try {
            return renderFunction.apply(this, arguments);
        }
        catch (e) {
            console.error('Intercepted @ ' + where, e, this);
            if (component != null) {
                return component;
            }
            // Just redefine this logic here to return default custom error-handling component
            Core.Actions.InterfaceActions.crash();
            return null;
        }
    };
};

module.exports = Interceptor;

and use it like this:

var interceptor = require('Interceptor');
...
render: interceptor('ThisIsMyComponentName.render', function() {
  var a = b.c; // Raising exception...
  return <div>Hello world!</div>
})

Of course, this is just a workaround and the disadvantage of it is that you have to wrap all render functions with this interceptor in your project… but at least your app won’t break on any tiny exception.

Is there a PR to track for this? Just want to throw in my thanks on this as well, you all are killing it as usual.