React re-executes render() right after an exception has been thrown in the same render() method
See original GitHub issueDo you want to request a feature or report a bug? I would like to clarify a doubt regarding this behaviour (maybe a bug? I don’t know).
What is the current behavior? By playing with the Error Boundaries demo:
https://codepen.io/gaearon/pen/wqvxGa?editors=0010
I discovered that when an exception is thrown in render()
, React re-executes that same render()
method before running the corresponding Error Boundary’s code.
Check out the same code with a console.log
line added both to BuggyCounter.render()
and componentDidCatch()
:
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { error: null, errorInfo: null };
}
componentDidCatch(error, errorInfo) {
// Catch errors in any components below and re-render with error message
console.log('componentDidCatch') // <------------------- LOG
this.setState({
error: error,
errorInfo: errorInfo
})
// You can also log error messages to an error reporting service here
}
render() {
if (this.state.errorInfo) {
// Error path
return (
<div>
<h2>Something went wrong.</h2>
<details style={{ whiteSpace: 'pre-wrap' }}>
{this.state.error && this.state.error.toString()}
<br />
{this.state.errorInfo.componentStack}
</details>
</div>
);
}
// Normally, just render children
return this.props.children;
}
}
class BuggyCounter extends React.Component {
constructor(props) {
super(props);
this.state = { counter: 0 };
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.setState(({counter}) => ({
counter: counter + 1
}));
}
render() {
console.log('this.state.counter', this.state.counter) // <------------------- LOG
if (this.state.counter === 5) {
// Simulate a JS error
throw new Error('I crashed!');
}
return <h1 onClick={this.handleClick}>{this.state.counter}</h1>;
}
}
function App() {
return (
<div>
<p>
<b>
This is an example of error boundaries in React 16.
<br /><br />
Click on the numbers to increase the counters.
<br />
The counter is programmed to throw when it reaches 5. This simulates a JavaScript error in a component.
</b>
</p>
<hr />
<ErrorBoundary>
<p>These two counters are inside the same error boundary. If one crashes, the error boundary will replace both of them.</p>
<BuggyCounter />
<BuggyCounter />
</ErrorBoundary>
<hr />
<p>These two counters are each inside of their own error boundary. So if one crashes, the other is not affected.</p>
<ErrorBoundary><BuggyCounter /></ErrorBoundary>
<ErrorBoundary><BuggyCounter /></ErrorBoundary>
</div>
);
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
After the first rendering I see (OK):
After clicking on the counter 4 times (OK):
Then, after clicking once more (Why does render()
gets executed twice?):
After that, componentDidCatch()
is, as expected, executed only once.
What is the expected behavior?
Cannot tell. This is not a problem in se because of the idempotent nature of render()
(which has to be pure), but makes me think that this is an unneeded call because if an error has been thrown in render()
, re-executing it right away would still lead to the same exception being thrown.
Or, is there something I am missing? Maybe related to how React works internally and because of that it needs to recall render()
as soon as that same render()
has thrown…
Which versions of React, and which browser / OS are affected by this issue? Did this work in previous versions of React?
OS: OS X Browser: Chrome 77 Using the same React 16 development version of the Pen:
https://unpkg.com/react@16/umd/react.development.js
https://unpkg.com/react-dom@16/umd/react-dom.development.js
Thank you for the attention.
Issue Analytics
- State:
- Created 4 years ago
- Reactions:1
- Comments:7 (2 by maintainers)
Yes, React will call it twice. There are some obscure reasons why we do this. As you said, due to idempotency of render it shouldn’t matter.
Tried to do that, it happens within the
invokeGuardedCallback
function:Still didn’t understand why, though.
Anyway, thanks for replying. 😃