Issue 1:
We are using error boundaries to catch and handle errors inside of a React render. As for errors which occur outside of a React render (i.e. uncaught errors that happen in a timeout), we need to register an uncaught error handler (window.addEventListener('error', …)) to handle these separately (otherwise the user will not be alerted).

Unfortunately we can only rely on this uncaught error event handler in production, not development. This is because—in development—errors caught by error boundaries will still bubble up to the uncaught error handler. (Unfortunately it's not documented but it's explained here: #12897 (comment). PR to add this to the docs: reactjs/react.dev#2168.)

This means that (in development) the uncaught error handler will be called for errors which are actually caught by error boundaries.

Intuitively I expect boundaries to work like try...catch—if an error is caught, it should never bubble up to the uncaught error handler. Also, I do not expect such a big difference in the way errors are handled in development versus production.

For the same reason, when using the Fast Refresh webpack plugin in development, the error overlay still appears even when an error is caught by an error boundary, because they are using an uncaught error handler to detect when to show the overlay. I would not expect this to happen because the error overlay is traditionally for uncaught errors.

Why does React need to treat errors differently in development? Is there a reason it needs to do this?

Whatever the reason, I think it's quite important for errors to behave consistently in development and production. When we're working on the user experience around errors in development, it's not obvious that the behaviour of errors could be significantly different in production. Furthermore, it's difficult to test error boundaries in development if an uncaught error handler has also been registered. For example, if Fast Refresh is used, the error overlay will appear over the top of the error boundary.

For this reason, I'm keen to explore if there is a different approach we can take here, in order to make the behaviour consistent between development and production. Ideally error boundaries would just behave similarly to try...catch.

Failing that, is there any other way we can differentiate between "error caught by boundary" and "uncaught error"—which works in production and development?

To demonstrate the problem, here is a reduced test case which:

throws an error both inside and outside of React render

registers a uncaught error handler and logs events

uses an error boundary and logs calls to componentDidCatch and getDerivedStateFromError

From the log screenshots I've included at the bottom, we can see that in development the uncaught error handler is called for the error which is in-fact caught by an error boundary.

src/index.jsx:

import * as React from 'react';
import * as ReactDOM from 'react-dom';

window.addEventListener('error', (event) => {
console.log('window.addEventListener error', event);
});

class ErrorBoundary extends React.Component {
state = { hasError: false };

static getDerivedStateFromError(error) {
console.log('getDerivedStateFromError', { error });

return { hasError: true };
}

componentDidCatch(error, errorInfo) {
console.log('componentDidCatch', { error, errorInfo });
}

render() {
return this.state.hasError === false && this.props.children;
}
}

const Boom = () => {
console.log('Boom');

throw new Error('Error inside React render');

return null;
};

const App = () => {
console.log('App render');

return (
<ErrorBoundary>
<Boom />
</ErrorBoundary>
);
};

const rootEl = document.getElementById('root');

ReactDOM.render(<App />, rootEl);

setTimeout(() => {
throw new Error('Error outside of React render');
}, 5000);

Development

Production

Related: https://stackoverflow.com/questions/57197936/componentdidcatch-window-addeventlistenererror-cb-dont-behave-as-expecte/57200935

Issue 2:
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.