Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Error boundaries: Recover from errors thrown in render #2461

Closed
longlho opened this issue Nov 4, 2014 · 85 comments
Closed

Error boundaries: Recover from errors thrown in render #2461

longlho opened this issue Nov 4, 2014 · 85 comments

Comments

@longlho
Copy link

longlho commented Nov 4, 2014

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
Copy link
Contributor

syranide commented Nov 4, 2014

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
Copy link
Author

longlho commented Nov 4, 2014

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
Copy link
Contributor

syranide commented Nov 4, 2014

@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
Copy link
Collaborator

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.

@sophiebits sophiebits changed the title Can't recover from error in render function Error boundaries: Recover from errors thrown in render Nov 4, 2014
@longlho
Copy link
Author

longlho commented Nov 6, 2014

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
Copy link
Contributor

jimfb commented Mar 5, 2015

#3313 has a really nice unit test that we should probably start using once the error boundaries issue is fixed.

@pedroteixeira
Copy link

👍

1 similar comment
@samzscott
Copy link

+1

@moosingin3space
Copy link

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

@jnak
Copy link

jnak commented Jun 11, 2015

+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
Copy link

skiano commented Jun 22, 2015

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
Copy link
Collaborator

@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
Copy link

+1

@ghost
Copy link

ghost commented Jul 22, 2015

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.

dmohs added a commit to dmohs/react-cljs that referenced this issue Aug 7, 2015
If a component's render function throws an error, React cannot recover. This causes subsequent hot reloads to fail. Now we catch these errors during hot reloading so things are moar better.
@mhagmajer
Copy link

+1

@nmartin413
Copy link

+1

1 similar comment
@aleclarson
Copy link
Contributor

👍

@gaearon
Copy link
Collaborator

gaearon commented Oct 19, 2015

@spicyj Should we label this as big-picture?

@sophiebits
Copy link
Collaborator

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

@benmosher
Copy link

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

@LeZuse
Copy link

LeZuse commented Nov 22, 2015

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:

  • render an error string instead of the component (might be good for __DEV__===true)
  • render an empty <span> (good for __DEV__===false)
  • log the thrown error into the console (invariant)

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

@DouglasLivingstone
Copy link

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!

@bywo
Copy link

bywo commented Jun 12, 2017

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/

@gaearon
Copy link
Collaborator

gaearon commented Jun 26, 2017

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.

@gaearon
Copy link
Collaborator

gaearon commented Jun 26, 2017

@slorber If error boundaries don't work in latest alpha for some reason please create a complete reproducing example.

@kossnocorp
Copy link

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>
  }
}

@gaearon
Copy link
Collaborator

gaearon commented Jun 26, 2017

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.

@natew
Copy link

natew commented Jun 26, 2017 via email

@gaearon
Copy link
Collaborator

gaearon commented Jun 26, 2017

That warrants a separate discussion (it's not immediately clear to me how hot reloading should work with error handling). We might be looking at hot reloading some time this year, and this is a good time to discuss the ideal experience.

@joshclowater
Copy link

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));

@gaearon
Copy link
Collaborator

gaearon commented Jun 27, 2017

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

<ErrorBoundary>
  <Something />
</ErrorBoundary>

This gives you more freedom with granularity.

@joshclowater
Copy link

@gaearon Personal preference I guess. I find it slightly cleaner as it abstracts the error handling out of the JSX. If I'm wrapping many components, it could noisy to parse through all of the <ErrorBoundary>s. Also, if I know that's how I want the component to handle errors, then I don't have to worry about the parent containers wrapping it with <ErrorBoundary>...<ErrorBoundary> and risk missing it somewhere else.

<ErrorBoundary>
  <Something />
</ErrorBoundary>
<ErrorBoundary>
  <Something />
</ErrorBoundary>

...

<Something /> // Oops missed that one

@jedwards1211
Copy link
Contributor

jedwards1211 commented Jul 7, 2017

Every time I get an error in a render method I have to scroll up through dozens of errors from React being in a broken state to find the error from my code that actually blew it up. 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.

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.

@jedwards1211
Copy link
Contributor

jedwards1211 commented Jul 7, 2017

Perhaps concern about the try-catch performance penalty will soon be obsolete: https://stackoverflow.com/a/19728876/200224

Not only does one comment mention that recent versions of V8 can optimize functions containing try-catch blocks, that answer explains that most of the performance penalty can be avoided by moving the try-catch into a wrapper function so that the main function which could throw can be optimized.

From Petka Antonov's optimization killers guide:

To use such statements with minimal impact, they must be isolated to a minimal function so that the main code is not affected:

var errorObject = {value: null};
function tryCatch(fn, ctx, args) {
    try {
        return fn.apply(ctx, args);
    }
    catch(e) {
        errorObject.value = e;
        return errorObject;
    }
}

var result = tryCatch(mightThrow, void 0, [1,2,3]);
//Unambiguously tells whether the call threw
if(result === errorObject) {
    var error = errorObject.value;
}
else {
    //result is the returned value
}

@gaearon
Copy link
Collaborator

gaearon commented Jul 7, 2017

@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: #2461 (comment). This is pretty much what we’ve added.

@jedwards1211
Copy link
Contributor

jedwards1211 commented Jul 7, 2017

@gaearon oh, I see. That will be nice, though I assume I will probably end up using react-guard (which I just discovered from this thread) more than something like <ErrorBoundary> for dev mode.

@jedwards1211
Copy link
Contributor

...nevermind, I thought react-guard made it possible to display a redbox in place of the component that failed, but it appears to display the redbox in place of the root component.

@gaearon
Copy link
Collaborator

gaearon commented Jul 7, 2017

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.

@bvaughn
Copy link
Contributor

bvaughn commented Jul 8, 2017

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

@btnwtn
Copy link

btnwtn commented Jul 17, 2017

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

@gaearon
Copy link
Collaborator

gaearon commented Jul 17, 2017

No. We settled on componentDidCatch.

@gaearon gaearon marked this as a duplicate of #10215 Jul 19, 2017
@ivansglazunov
Copy link

ivansglazunov commented Jul 25, 2017

@gaearon componentDidCatch just for current component or for childs too?
In the case of an unknown source of components, for the possibility of dismantling, it is important to catch children from parents, too.

@gaearon
Copy link
Collaborator

gaearon commented Jul 25, 2017

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

@bvaughn bvaughn mentioned this issue Jul 26, 2017
@gaearon
Copy link
Collaborator

gaearon commented Jul 26, 2017

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests