facebook / react

The library for web and native user interfaces.
https://react.dev
MIT License
229.04k stars 46.86k forks source link

Error boundaries: Recover from errors thrown in `render` #2461

Closed longlho closed 7 years ago

longlho commented 10 years ago

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?

syranide commented 10 years ago

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.

longlho commented 10 years ago

Ah thanks :) I mean it's not a matter of trust it's to prevent using 3rd-party React component and whatnot. Do we plan to have more support for error handling?

syranide commented 10 years ago

@longlho Not my area, but I believe the two biggest blockers are; many browsers ruin the error/call-stack when rethrowing and try-catch incurs performance penalities and these are hot paths.

sophiebits commented 10 years ago

Yeah, you may hear conversation about "error boundaries" which means making a way to handle this sort of thing more gracefully and something we want to find a good solution to. I don't think we have a tracking issue for it though so let's use this one.

longlho commented 10 years ago

Ah thanks guys :) this does sound like a big issue but definitely important. Given that react is stateful wouldn't it require some sort of sandbox for rendering to make sure everything's cool, then apply the new state I guess? Is there any active effort on this front?

jimfb commented 9 years ago

https://github.com/facebook/react/issues/3313 has a really nice unit test that we should probably start using once the error boundaries issue is fixed.

pedroteixeira commented 9 years ago

:+1:

samzscott commented 9 years ago

+1

moosingin3space commented 9 years ago

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

jnak commented 9 years ago

+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.

skiano commented 9 years ago

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?

sophiebits commented 9 years ago

@skiano If that's working for you, no reason to stop. Our eventual solution should be a little more flexible than this and more efficient but will be similar in spirit.

conoremclaughlin commented 9 years ago

+1

ghost commented 9 years ago

Any idea what the time-frame might be for an official solution? Just curious. :) In the mean-time I will probably just monkey-patch everything.

mhagmajer commented 9 years ago

+1

nmartin413 commented 9 years ago

+1

aleclarson commented 9 years ago

:+1:

gaearon commented 9 years ago

@spicyj Should we label this as big-picture?

sophiebits commented 9 years ago

I'm willing to but it doesn't require much discussion, it just needs to get done.

benmosher commented 9 years ago

I'd be glad to help out with this. It's a pain point for my team.

LeZuse commented 8 years ago

Hi, first of all the test case in the first post works only within certain app context. Here's the working test case http://jsfiddle.net/XeeD/pLqphjm4/.

There is an already mounted component, that will throw in render() for the second render pass which leaves React in an inconsistent state, breaking the code here in ReactReconciler.unmountComponent

We thought about a few approaches by catching it here in ReactCompositeComponentMixin._renderValidatedComponent and:

However we are not sure about the correct way how to handle this. Feedback? Working on PR atm.

ngasull commented 8 years ago

+1, this would be a huge improvement

yagudaev commented 8 years ago

+1, been thinking about this lately

slorber commented 8 years ago

very interested by this issue too.

Any idea on how the problem can be solved efficiently?

sophiebits commented 8 years ago

Let's leave this open until the new feature is complete.

mhagmajer commented 8 years ago

What is the expected behavior here? Are we going to propagate the error to the top-most component?

sophiebits commented 8 years ago

An error bubbles up to the nearest error boundary (that is, a component with unstable_handleError defined). If that component errors when trying to handle the error, the error will keep rising.

natew commented 8 years ago

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.

sophiebits commented 8 years ago

5602 has the first code for this which catches errors only on initial render but not at any other time. When we have more I am sure that this issue number will be mentioned in the description so you will see them in the comment history here.

cappslock commented 8 years ago

+1, just got bit by this :)

mik01aj commented 8 years ago

Well, there's react-transform-catch-errors for those using Babel, but I'd still like to see it fixed directly in React. :+1: from me.

joaomilho commented 8 years ago

+1

DanielSundberg commented 8 years ago

My team just ran into this issue and found this thread.

Some comments in here hint that the react team is working on improving this...

Can anyone comment if there's any progress being made or if this perhaps is on the roadmap for 15.0 or a later release?

jimfb commented 8 years ago

@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.

vguzev commented 8 years ago

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.

chandlerprall commented 8 years ago

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

thom-nic commented 8 years ago

Can anyone point to an example of unstable_handleError use in v15? I can't seem to figure out how to make it work :/

jimfb commented 8 years ago

@thom-nic Officially, v15 does not support error boundaries. Unofficially, https://github.com/facebook/react/blob/master/src/core/__tests__/ReactErrorBoundaries-test.js

thom-nic commented 8 years ago

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.

Aldredcz commented 8 years ago

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

0x7aF777 commented 8 years ago

Hi, @Aldredcz Thanks for your shearing. What is the licence of monkeypatching? Could I use it in my project?

Aldredcz commented 8 years ago

@wangyangjun use it anywhere you wish :)

tomchentw commented 8 years ago

The updated link to unstable_handleError in v15 is here: https://github.com/facebook/react/blob/15-stable/src/renderers/shared/stack/reconciler/__tests__/ReactErrorBoundaries-test.js

ahmedyahiaalkaff commented 8 years ago

Hi, I get the following error when I try to use unstable_handleError: [Error] TypeError: null is not an object (evaluating 'this._renderedComponent.unmountComponent') (anonymous function) (comp.js, line 15788)

ivansglazunov commented 8 years ago

@Aldredcz It's really great work, thanks!

bitmage commented 7 years ago

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.

hyperh commented 7 years ago

+1

sophiebits commented 7 years ago

@bitmage That's true; if you implement unstable_handleError then React doesn't log anything or rethrow the error; it is up to you to do so. It is like a try/catch. In a future version of React we will log an error regardless though (see #8756 for a preview).

slorber commented 7 years ago

@spicyj are there cases where the unstable_handleError is not called?

I'm trying to build an HOC like that but the callback does not seem to be ever called:

import React from "react";

const RenderFallback = () => (
  <div>error</div>
);

// See https://github.com/facebook/react/issues/2461
export const ErrorBoundaryConstructor = ({
  fallbackComponent: RenderFallback,
  onError = undefined,
} = {}) => (WrappedComponent) => {

  return class ErrorBoundary extends React.Component {

    constructor(props) {
      super(props);
      this.state = {error: false};
    }

    unstable_handleError(e) {
      console.warn("unstable_handleError",e);
      onError && onError(e);
      this.setState({error: true});
    }

    render() {
      const Comp = this.state.error ? fallbackComponent : WrappedComponent;
      return <Comp {...this.props}/>;
    }

  }

};

export const DefaultErrorBoundary = Component => {
  return ErrorBoundaryConstructor()(Component);
};

An example of client code:

import React from "react";
import Portal from "react-portal";
import classNames from "classnames";
import {DefaultErrorBoundary} from "utils/errorBoundary";

const FullscreenPortal = React.createClass({
  render() {
    return (
      <Portal isOpened={true}
              className={classNames(this.props.className, "fullscreen-portal")}
      >
        {this.props.children}
      </Portal>
    )
  },
});
export default DefaultErrorBoundary(FullscreenPortal);
sophiebits commented 7 years ago

Where is your error thrown? I don't see it in your code.