<p dir="auto">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 (<code class="notranslate">window.addEventListener('error', …)</code>) to handle these separately (otherwise the user will not be alerted).</p>
<p dir="auto">Unfortunately we can only rely on this uncaught error event handler in production, not development. This is because—<strong>in development—errors caught by error boundaries will still bubble up to the uncaught error handler</strong>. (Unfortunately it's not documented but it's explained here: <a class="issue-link js-issue-link" data-error-text="Failed to load title" data-id="326048819" data-permission-text="Title is private" data-url="https://github.com/facebook/react/issues/12897" data-hovercard-type="issue" data-hovercard-url="/facebook/react/issues/12897/hovercard?comment_id=410036991&amp;comment_type=issue_comment" href="https://github.com/facebook/react/issues/12897#issuecomment-410036991">#12897 (comment)</a>. PR to add this to the docs: <a class="issue-link js-issue-link" data-error-text="Failed to load title" data-id="472825619" data-permission-text="Title is private" data-url="https://github.com/reactjs/react.dev/issues/2168" data-hovercard-type="pull_request" data-hovercard-url="/reactjs/react.dev/pull/2168/hovercard" href="https://github.com/reactjs/react.dev/pull/2168">reactjs/react.dev#2168</a>.)</p>
<p dir="auto">This means that (in development) the uncaught error handler will be called for errors which are actually caught by error boundaries.</p>
<p dir="auto"><strong>Intuitively I expect boundaries to work like <code class="notranslate">try...catch</code>—if an error is caught, it should never bubble up to the uncaught error handler.</strong> Also, I do not expect such a big difference in the way errors are handled in development versus production.</p>
<p dir="auto">For the same reason, when using the <a href="https://github.com/pmmmwh/react-refresh-webpack-plugin">Fast Refresh webpack plugin</a> 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 <em>uncaught</em> errors.</p>
<p dir="auto"><strong><em>Why</em> does React need to treat errors differently in development? Is there a reason it needs to do this?</strong></p>
<p dir="auto">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.</p>
<p dir="auto">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. <strong>Ideally error boundaries would just behave similarly to <code class="notranslate">try...catch</code>.</strong></p>
<p dir="auto">Failing that, is there any other way we can differentiate between "error caught by boundary" and "uncaught error"—which works in production <em>and</em> development?</p>
<p dir="auto">To demonstrate the problem, <a href="https://github.com/OliverJAsh/react-error-boundary-uncaught-issue">here is a reduced test case</a> which:</p>
<ul dir="auto">
<li>throws an error both inside and outside of React render</li>
<li>registers a uncaught error handler and logs events</li>
<li>uses an error boundary and logs calls to <code class="notranslate">componentDidCatch</code> and <code class="notranslate">getDerivedStateFromError</code></li>
</ul>
<p dir="auto">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.</p>
<p dir="auto"><code class="notranslate">src/index.jsx</code>:</p>
<div class="highlight highlight-source-js notranslate position-relative overflow-auto" dir="auto" data-snippet-clipboard-copy-content="import * as React from 'react';
import * as ReactDOM from 'react-dom';

window.addEventListener('error', (event) =&gt; {
    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 &amp;&amp; this.props.children;
    }
}

const Boom = () =&gt; {
    console.log('Boom');

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

    return null;
};

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

    return (
        &lt;ErrorBoundary&gt;
            &lt;Boom /&gt;
        &lt;/ErrorBoundary&gt;
    );
};

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

ReactDOM.render(&lt;App /&gt;, rootEl);

setTimeout(() =&gt; {
    throw new Error('Error outside of React render');
}, 5000);"><pre class="notranslate"><span class="pl-k">import</span> <span class="pl-c1">*</span> <span class="pl-k">as</span> <span class="pl-v">React</span> <span class="pl-k">from</span> <span class="pl-s">'react'</span><span class="pl-kos">;</span>
<span class="pl-k">import</span> <span class="pl-c1">*</span> <span class="pl-k">as</span> <span class="pl-v">ReactDOM</span> <span class="pl-k">from</span> <span class="pl-s">'react-dom'</span><span class="pl-kos">;</span>

<span class="pl-smi">window</span><span class="pl-kos">.</span><span class="pl-en">addEventListener</span><span class="pl-kos">(</span><span class="pl-s">'error'</span><span class="pl-kos">,</span> <span class="pl-kos">(</span><span class="pl-s1">event</span><span class="pl-kos">)</span> <span class="pl-c1">=&gt;</span> <span class="pl-kos">{</span>
    <span class="pl-smi">console</span><span class="pl-kos">.</span><span class="pl-en">log</span><span class="pl-kos">(</span><span class="pl-s">'window.addEventListener error'</span><span class="pl-kos">,</span> <span class="pl-s1">event</span><span class="pl-kos">)</span><span class="pl-kos">;</span>
<span class="pl-kos">}</span><span class="pl-kos">)</span><span class="pl-kos">;</span>

<span class="pl-k">class</span> <span class="pl-v">ErrorBoundary</span> <span class="pl-k">extends</span> <span class="pl-v">React</span><span class="pl-kos">.</span><span class="pl-c1">Component</span> <span class="pl-kos">{</span>
    <span class="pl-c1">state</span> <span class="pl-c1">=</span> <span class="pl-kos">{</span> <span class="pl-c1">hasError</span>: <span class="pl-c1">false</span> <span class="pl-kos">}</span><span class="pl-kos">;</span>

    <span class="pl-k">static</span> <span class="pl-en">getDerivedStateFromError</span><span class="pl-kos">(</span><span class="pl-s1">error</span><span class="pl-kos">)</span> <span class="pl-kos">{</span>
        <span class="pl-smi">console</span><span class="pl-kos">.</span><span class="pl-en">log</span><span class="pl-kos">(</span><span class="pl-s">'getDerivedStateFromError'</span><span class="pl-kos">,</span> <span class="pl-kos">{</span> error <span class="pl-kos">}</span><span class="pl-kos">)</span><span class="pl-kos">;</span>

        <span class="pl-k">return</span> <span class="pl-kos">{</span> <span class="pl-c1">hasError</span>: <span class="pl-c1">true</span> <span class="pl-kos">}</span><span class="pl-kos">;</span>
    <span class="pl-kos">}</span>

    <span class="pl-en">componentDidCatch</span><span class="pl-kos">(</span><span class="pl-s1">error</span><span class="pl-kos">,</span> <span class="pl-s1">errorInfo</span><span class="pl-kos">)</span> <span class="pl-kos">{</span>
        <span class="pl-smi">console</span><span class="pl-kos">.</span><span class="pl-en">log</span><span class="pl-kos">(</span><span class="pl-s">'componentDidCatch'</span><span class="pl-kos">,</span> <span class="pl-kos">{</span> error<span class="pl-kos">,</span> errorInfo <span class="pl-kos">}</span><span class="pl-kos">)</span><span class="pl-kos">;</span>
    <span class="pl-kos">}</span>

    <span class="pl-en">render</span><span class="pl-kos">(</span><span class="pl-kos">)</span> <span class="pl-kos">{</span>
        <span class="pl-k">return</span> <span class="pl-smi">this</span><span class="pl-kos">.</span><span class="pl-c1">state</span><span class="pl-kos">.</span><span class="pl-c1">hasError</span> <span class="pl-c1">===</span> <span class="pl-c1">false</span> <span class="pl-c1">&amp;&amp;</span> <span class="pl-smi">this</span><span class="pl-kos">.</span><span class="pl-c1">props</span><span class="pl-kos">.</span><span class="pl-c1">children</span><span class="pl-kos">;</span>
    <span class="pl-kos">}</span>
<span class="pl-kos">}</span>

<span class="pl-k">const</span> <span class="pl-v">Boom</span> <span class="pl-c1">=</span> <span class="pl-kos">(</span><span class="pl-kos">)</span> <span class="pl-c1">=&gt;</span> <span class="pl-kos">{</span>
    <span class="pl-smi">console</span><span class="pl-kos">.</span><span class="pl-en">log</span><span class="pl-kos">(</span><span class="pl-s">'Boom'</span><span class="pl-kos">)</span><span class="pl-kos">;</span>

    <span class="pl-k">throw</span> <span class="pl-k">new</span> <span class="pl-v">Error</span><span class="pl-kos">(</span><span class="pl-s">'Error inside React render'</span><span class="pl-kos">)</span><span class="pl-kos">;</span>

    <span class="pl-k">return</span> <span class="pl-c1">null</span><span class="pl-kos">;</span>
<span class="pl-kos">}</span><span class="pl-kos">;</span>

<span class="pl-k">const</span> <span class="pl-v">App</span> <span class="pl-c1">=</span> <span class="pl-kos">(</span><span class="pl-kos">)</span> <span class="pl-c1">=&gt;</span> <span class="pl-kos">{</span>
    <span class="pl-smi">console</span><span class="pl-kos">.</span><span class="pl-en">log</span><span class="pl-kos">(</span><span class="pl-s">'App render'</span><span class="pl-kos">)</span><span class="pl-kos">;</span>

    <span class="pl-k">return</span> <span class="pl-kos">(</span>
        <span class="pl-c1">&lt;</span><span class="pl-ent">ErrorBoundary</span><span class="pl-c1">&gt;</span>
            <span class="pl-c1">&lt;</span><span class="pl-v">Boom</span> <span class="pl-c1">/</span><span class="pl-c1">&gt;</span>
        <span class="pl-c1">&lt;</span><span class="pl-c1">/</span><span class="pl-ent">ErrorBoundary</span><span class="pl-c1">&gt;</span>
    <span class="pl-kos">)</span><span class="pl-kos">;</span>
<span class="pl-kos">}</span><span class="pl-kos">;</span>

<span class="pl-k">const</span> <span class="pl-s1">rootEl</span> <span class="pl-c1">=</span> <span class="pl-smi">document</span><span class="pl-kos">.</span><span class="pl-en">getElementById</span><span class="pl-kos">(</span><span class="pl-s">'root'</span><span class="pl-kos">)</span><span class="pl-kos">;</span>

<span class="pl-v">ReactDOM</span><span class="pl-kos">.</span><span class="pl-en">render</span><span class="pl-kos">(</span><span class="pl-c1">&lt;</span><span class="pl-v">App</span> <span class="pl-c1">/</span><span class="pl-c1">&gt;</span><span class="pl-kos">,</span> <span class="pl-s1">rootEl</span><span class="pl-kos">)</span><span class="pl-kos">;</span>

<span class="pl-en">setTimeout</span><span class="pl-kos">(</span><span class="pl-kos">(</span><span class="pl-kos">)</span> <span class="pl-c1">=&gt;</span> <span class="pl-kos">{</span>
    <span class="pl-k">throw</span> <span class="pl-k">new</span> <span class="pl-v">Error</span><span class="pl-kos">(</span><span class="pl-s">'Error outside of React render'</span><span class="pl-kos">)</span><span class="pl-kos">;</span>
<span class="pl-kos">}</span><span class="pl-kos">,</span> <span class="pl-c1">5000</span><span class="pl-kos">)</span><span class="pl-kos">;</span></pre></div>
<h3 dir="auto">Development</h3>
<p dir="auto"><a target="_blank" rel="noopener noreferrer nofollow" href="https://user-images.githubusercontent.com/921609/90268665-4afb7e80-de4f-11ea-9f1a-abdb21b655d9.png"><img src="https://user-images.githubusercontent.com/921609/90268665-4afb7e80-de4f-11ea-9f1a-abdb21b655d9.png" alt="image" style="max-width: 100%;"></a></p>
<h3 dir="auto">Production</h3>
<p dir="auto"><a target="_blank" rel="noopener noreferrer nofollow" href="https://user-images.githubusercontent.com/921609/90268721-5a7ac780-de4f-11ea-9274-c0f6a86d1b77.png"><img src="https://user-images.githubusercontent.com/921609/90268721-5a7ac780-de4f-11ea-9274-c0f6a86d1b77.png" alt="image" style="max-width: 100%;"></a></p>
<hr>
<p dir="auto">Related: <a href="https://stackoverflow.com/questions/57197936/componentdidcatch-window-addeventlistenererror-cb-dont-behave-as-expecte/57200935" rel="nofollow">https://stackoverflow.com/questions/57197936/componentdidcatch-window-addeventlistenererror-cb-dont-behave-as-expecte/57200935</a></p>
<p dir="auto">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.</p>
<p dir="auto">The problem seems to stem from <code class="notranslate">invokeGuardedCallbackDev</code> in <code class="notranslate">ReactErrorUtils.js</code>. I think that this entire <code class="notranslate">__DEV__</code> block of code is problematic. The stated rational is:</p>
<div class="snippet-clipboard-content notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="  // 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
  // &quot;Pause on exceptions&quot; 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."><pre class="notranslate"><code class="notranslate">  // 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.
</code></pre></div>
<p dir="auto">This is misguided because it's not about pausing on exceptions, it's about "pause on <em>uncaught</em> exceptions." However, <code class="notranslate">componentDidCatch</code> makes exceptions <em>caught</em>!</p>
<p dir="auto">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.</p>