react: The fake event trick for rethrowing errors in DEV fires unexpected global error handlers and makes testing harder
I’m trying to make use of componentDidCatch in the React 16 beta. I already had a global window error handler which was working fine, but it unexpectedly catches errors that I would expect componentDidCatch to have handled. That is, component-local errors are being treated as window-global errors in dev builds.
The problem seems to stem from invokeGuardedCallbackDev
in ReactErrorUtils.js
. I think that this entire __DEV__
block of code is problematic. The stated rational is:
// In DEV mode, we swap out invokeGuardedCallback for a special version
// that plays more nicely with the browser's DevTools. The idea is to preserve
// "Pause on exceptions" behavior. Because React wraps all user-provided
// functions in invokeGuardedCallback, and the production version of
// invokeGuardedCallback uses a try-catch, all user exceptions are treated
// like caught exceptions, and the DevTools won't pause unless the developer
// takes the extra step of enabling pause on caught exceptions. This is
// untintuitive, though, because even though React has caught the error, from
// the developer's perspective, the error is uncaught.
This is misguided because it’s not about pausing on exceptions, it’s about “pause on uncaught exceptions.” However, componentDidCatch
makes exceptions caught!
Rather than switching on prod vs dev and using try/catch in prod and window’s error handler in dev, React should always use try/catch, but rethrow if you reach the root without hitting a componentDidCatch handler. This would preserve the correct “pause on uncaught exceptions” behavior without messing with global error handlers.
About this issue
- Original URL
- State: open
- Created 7 years ago
- Reactions: 49
- Comments: 48 (13 by maintainers)
Workaround:
Any updates on this? The docs aren’t still very clear that this is the behavior in development. I spent a lot of time head scratching and wondering why my error boundaries didn’t catch the error and I still got our general error handler. It wasn’t until i googled my way here that I actually understood what the trouble was.
It would be super useful for this to be configurable or at least the errors tagged in some way so we can discard them in the general error handler as handled.
Personally I think it’s a bad idea to behave differently in development and production. It’s confusing for the developer, not to mention any non developers testing on development builds, like testers and UX.
A top-level component error boundary is fine for errors that occur during rendering or lifecycle callbacks, but does not address code that responds to network or other top-level events. For that, you need to hook
window.onerror
, which is precisely how I ran in to this problem.Sadly this doesn’t play nice with other tools like Cypress which fully stops on uncaught errors which confused us a lot initially when tests run fine in production, but not locally.
I’d just like to point out that that behavior should be clearly explained in the error boudaries / componentDidCatch documentation (it is not at the moment).
I had missing error logs in production builds because I was using the window error event to capture errors globally. It was working in dev, but not in production with error boundaries (which seems like the right thing to do, but confusing when the different behavior is not documented).
It seems really unintuitive that React calls it
componentDidCatch
, when in reality it is not “catching” it is more ofcomponentOnError
.As some context on the scenario this is causing issues for us on. Now that we’ve added intelligent error boundaries to our app, we want to start using that same boundary to handle an API error. So we are throwing when we hit an API error, and then it bubbles up to the nearest error boundary, and is handled just like any other error. Unfortunately, in dev, this causes redbox to appear, and since our dev environment is complex, a couple api calls pretty regularly fail. So this redbox appearing a couple seconds into every page load is extremely frustrating.
Hmmm… I totally agree with @mweststrate. I wasted about half a day researching why, after adding error boundaries to our app and through live testing seeing the errors get caught/handled, the fallback element wasn’t being rendered and instead the error propagated further and the app crashed. This isn’t the expected behavior based on the error boundary description. Perhaps it should be mentioned in the docs that by design the fallback only gets rendered in production. I too wanted to test the fallback rendering should an invalid prop be passed to a child component or the child component falls over. We should be able to test error boundary branching and ‘control flow’, since that’s what it’s actually doing, from within the dev environment.
[edit 1/11/18] I must revise my complaint. The app wasn’t crashing and the fallback element was getting rendered, it’s just that a full screen error pops up, covering my app, where it wasn’t immediately or overtly clear it could be dismissed. Now knowing this is the intended behavior I don’t have an issue. Docs could be a little more informational regarding this.
@sebmarkbage In my system, there’s a bunch of background processing and, if it fails, future event handlers will be all out of sync and the system will have cascading failures. I could wrap every callback for every subsystem in try/catch, but asynchrony makes that not practical, so I hook window.onerror. I want to log error, to the server, of course, but I also want to display some fatal error screen. In dev, that would show a trace, in prod, that would be akin to a
:(
browser tab.No matter how React handles the error (printing to console, calling error boundary, re-throwing a top level exception, etc), the main DX causality I’m feeling with this system is that I can’t have access to the stack and environment conditions that caused an exception to be originally thrown.
I’d really appreciate if my “pause on uncaught exception” debugger flag would pause exactly on the original throw, with the entire context available, but I understand it may be impossible to support this case while also supporting error boundaries, unless there is a flag you can turn on on a
__DEV__
build of React to completely disable error boundaries.@thysultan no, as
fn
won’t throw the exception synchronously@gaearon Not really, as that would indicate that the component introducing the bug isn’t tested itself? But regardless of that question, the problem is that as lib author I cannot create an environment anymore in which I can sandbox and test exceptions (using error boundaries or any other means).
As lib author I want to test failure scenarios as well. For example that my component throws when it is wrongly configured (in my specific case; context lacks some expected information). Previously (<16) it was no problem to test this behavior (Probably because the error was thrown sync so I could wrap a
try / catch around ReactDOM.render
). But now I cannot prevent exceptions from escaping anymore.So I am fine with the new default, but I think an option to influence this behavior is missing (or any another strategy to test exception behavior?).
Its better to check the stack of a new error rather than the error that the event handler caught:
I had to do it this way because I’m using TanStack Query and I’m throwing on HTTP errors so I can use error boundaries to handle those in a general way. But that means that the error that is caught by the error boundary is actually a promise rejection, which may not have
invokeGuardedCallbackDev
in its stack.Chiming in here. Because of this issue we (I mean me) deployed some code to production that broke logging errors caught by an error boundary to Sentry.
Personally, I would prefer code behaving the same way in dev and prod over being able to pause of caught exceptions in the browser’s dev tools.
If anyone is curious how I worked around this for Sentry I did so by proxying the
captureException
method on the hub.I also had the problem that I first thought I did something wrong as I always see the last resort error view, which is triggered by a global “onerror” handler. At very least, the docs should mention this.
Another thing is that I cannot really believe global error handles are that rare, but lacking of real data as well. As React “only” catches the errors in renders, a global error handler needs to be used to catch all errors in a last resort manner. I personally did this already before, to catch expected errors in e.g. network errors in sagas.
See the test: https://github.com/mweststrate/react-error-boundaries-issue/blob/master/index.js#L23
Real browser/ DOM rendering / nested components / no test utils / no enzyme
I’d consider this a bug in the 3rd party library, just as I would if
_.takeWhile(xs, pred)
suppressed errors from calls topred
.The code you posted depends on the subtle order of execution for the callbacks.
Have you tried it? I suspect it won’t work because a handler installed at startup will run before the handler React installs and uninstalls during rendering and lifecycle method execution.
Ah, OK, understood. That is the only compelling argument I’ve heard thus far 😃
My experience is that pausing on exceptions is not viable in any JavaScript scenario I’ve ever encountered, but again, I admit I am atypical here.
Probably unnecessary aside: JavaScript, and indeed most exception handling mechanisms, are semantically broken. Go, by contrast, runs defer/recover logic prior to unwinding the stack, so that the context at the point of the original throw would be available at the point of the last uncaught rethrow.
Short of fixing every major browser’s debuggers, I don’t have a better solution to propose. However, I will say that I still think the disease is worse than the cure. As long as you get a stack trace, you can add a conditional debugger statement at the site of the original throw.