Skip to content

Commit c768fa0

Browse files
committed
Add console errors for potential nested infinite loop before throwing
Gather production data in a `enableInfiniteLoopDetection` experiment around existing potential infinite loops. We disabled `enableInfiniteLoopDetection` with facebook#31088 as it was introducing new crashes that were hard to debug, especially with sibling prewarming. One issue with the previous rollout is that we were introducing new crashes for components that may have settled after the 50 pass limit, but before a real crash or infinite loop. This happened more frequently after enabling sibling prewarming which in some cases increased render counts that may have already been close to the limit. This change enables a console error where we used to throw (at 50 nested renders) while continuing to throw an error if that is hit 10 times (500 total nested renders being the new limit for throwing). This is a temporary mechanism to allow us to collect more data about potential crashes in production, without introducing new crashes ourselves. The goal will be to design a smarter mechanism, maybe not counting prewarming passes for example, and/or adjusting the final limit to make it easier to enable this on existing apps.
1 parent ef8b6fa commit c768fa0

File tree

2 files changed

+77
-21
lines changed

2 files changed

+77
-21
lines changed

packages/react-dom/src/__tests__/ReactUpdates-test.js

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1796,13 +1796,16 @@ describe('ReactUpdates', () => {
17961796
return;
17971797
}
17981798
let setState;
1799-
function App() {
1800-
const [, _setState] = React.useState(0);
1799+
function App({eventuallySettle = false}) {
1800+
const [state, _setState] = React.useState(0);
18011801
setState = _setState;
1802+
if (eventuallySettle) {
1803+
return state < 55 ? <Child /> : null;
1804+
}
18021805
return <Child />;
18031806
}
18041807

1805-
function Child(step) {
1808+
function Child() {
18061809
// This will cause an infinite update loop, and a warning in dev.
18071810
setState(n => n + 1);
18081811
return null;
@@ -1819,6 +1822,18 @@ describe('ReactUpdates', () => {
18191822
'To locate the bad setState() call inside `Child`, ' +
18201823
'follow the stack trace as described in https://react.dev/link/setstate-in-render\n' +
18211824
' in App (at **)',
1825+
'Potential infinite loop detected. This can happen when a component ' +
1826+
'repeatedly calls setState in render. React limits the number of nested ' +
1827+
'updates to prevent infinite loops.',
1828+
]);
1829+
1830+
await act(() =>
1831+
ReactDOM.flushSync(() => root.render(<App eventuallySettle={true} />)),
1832+
);
1833+
assertConsoleErrorDev([
1834+
'Potential infinite loop detected. This can happen when a component ' +
1835+
'repeatedly calls setState in render. React limits the number of nested ' +
1836+
'updates to prevent infinite loops.',
18221837
]);
18231838
});
18241839

@@ -1827,13 +1842,16 @@ describe('ReactUpdates', () => {
18271842
return;
18281843
}
18291844
let setState;
1830-
function App() {
1831-
const [, _setState] = React.useState(0);
1845+
function App({eventuallySettle = false}) {
1846+
const [state, _setState] = React.useState(0);
18321847
setState = _setState;
1848+
if (eventuallySettle) {
1849+
return state < 80 ? <Child /> : null;
1850+
}
18331851
return <Child />;
18341852
}
18351853

1836-
function Child(step) {
1854+
function Child() {
18371855
// This will cause an infinite update loop, and a warning in dev.
18381856
setState(n => n + 1);
18391857
return null;
@@ -1853,6 +1871,18 @@ describe('ReactUpdates', () => {
18531871
'To locate the bad setState() call inside `Child`, ' +
18541872
'follow the stack trace as described in https://react.dev/link/setstate-in-render\n' +
18551873
' in App (at **)',
1874+
'Potential infinite loop detected. This can happen when a component ' +
1875+
'repeatedly calls setState in render. React limits the number of nested ' +
1876+
'updates to prevent infinite loops.',
1877+
]);
1878+
1879+
await act(() => {
1880+
React.startTransition(() => root.render(<App eventuallySettle={true} />));
1881+
});
1882+
assertConsoleErrorDev([
1883+
'Potential infinite loop detected. This can happen when a component ' +
1884+
'repeatedly calls setState in render. React limits the number of nested ' +
1885+
'updates to prevent infinite loops.',
18561886
]);
18571887
});
18581888

packages/react-reconciler/src/ReactFiberWorkLoop.js

Lines changed: 41 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -683,7 +683,9 @@ let pendingSuspendedCommitReason: SuspendedCommitReason = IMMEDIATE_COMMIT; // P
683683

684684
// Use these to prevent an infinite loop of nested updates
685685
const NESTED_UPDATE_LIMIT = 50;
686+
const NESTED_UPDATE_LIMIT_MAX_HITS = 10;
686687
let nestedUpdateCount: number = 0;
688+
let nestedUpdateCountWarningThresholdHits: number = 0;
687689
let rootWithNestedUpdates: FiberRoot | null = null;
688690
let isFlushingPassiveEffects = false;
689691
let didScheduleUpdateDuringPassiveEffects = false;
@@ -4532,23 +4534,47 @@ export function throwIfInfiniteUpdateLoopDetected() {
45324534

45334535
if (enableInfiniteRenderLoopDetection) {
45344536
if (executionContext & RenderContext && workInProgressRoot !== null) {
4535-
// We're in the render phase. Disable the concurrent error recovery
4536-
// mechanism to ensure that the error we're about to throw gets handled.
4537-
// We need it to trigger the nearest error boundary so that the infinite
4538-
// update loop is broken.
4539-
workInProgressRoot.errorRecoveryDisabledLanes = mergeLanes(
4540-
workInProgressRoot.errorRecoveryDisabledLanes,
4541-
workInProgressRootRenderLanes,
4542-
);
4537+
// For the enableInfiniteRenderLoopDetection experiment, we use a console
4538+
// error to warn of potential infinite loops while allowing more passes
4539+
// in an attempt to let the rendering settle.
4540+
// If NESTED_UPDATE_LIMIT * NESTED_UPDATE_LIMIT_MAX_HITS is reached,
4541+
// we throw an error to break the infinite loop.
4542+
// We expect we can simplify this in the future to throw on one render limit.
4543+
nestedUpdateCountWarningThresholdHits++;
4544+
if (
4545+
nestedUpdateCountWarningThresholdHits >= NESTED_UPDATE_LIMIT_MAX_HITS
4546+
) {
4547+
// We're in the render phase. Disable the concurrent error recovery
4548+
// mechanism to ensure that the error we're about to throw gets handled.
4549+
// We need it to trigger the nearest error boundary so that the infinite
4550+
// update loop is broken.
4551+
workInProgressRoot.errorRecoveryDisabledLanes = mergeLanes(
4552+
workInProgressRoot.errorRecoveryDisabledLanes,
4553+
workInProgressRootRenderLanes,
4554+
);
4555+
} else if (nestedUpdateCountWarningThresholdHits === 1) {
4556+
// This is the first time we hit the limit, so we log a warning.
4557+
console['error'](
4558+
'Potential infinite loop detected. This can happen when a component ' +
4559+
'repeatedly calls setState in render. React limits the number of nested ' +
4560+
'updates to prevent infinite loops.',
4561+
);
4562+
}
45434563
}
45444564
}
4545-
4546-
throw new Error(
4547-
'Maximum update depth exceeded. This can happen when a component ' +
4548-
'repeatedly calls setState inside componentWillUpdate or ' +
4549-
'componentDidUpdate. React limits the number of nested updates to ' +
4550-
'prevent infinite loops.',
4551-
);
4565+
if (
4566+
!enableInfiniteRenderLoopDetection ||
4567+
nestedUpdateCountWarningThresholdHits === 0 ||
4568+
nestedUpdateCountWarningThresholdHits >= NESTED_UPDATE_LIMIT_MAX_HITS
4569+
) {
4570+
nestedUpdateCountWarningThresholdHits = 0;
4571+
throw new Error(
4572+
'Maximum update depth exceeded. This can happen when a component ' +
4573+
'repeatedly calls setState inside componentWillUpdate or ' +
4574+
'componentDidUpdate. React limits the number of nested updates to ' +
4575+
'prevent infinite loops.',
4576+
);
4577+
}
45524578
}
45534579

45544580
if (__DEV__) {

0 commit comments

Comments
 (0)