From a24bbda511801ccc9f9d0547cb9f4850b313606e Mon Sep 17 00:00:00 2001 From: harshagarwalnyu Date: Sat, 9 May 2026 16:45:53 -0400 Subject: [PATCH 1/3] fix: throw error on useEffect infinite loops in production --- .../src/ReactFiberWorkLoop.js | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index d45a2d18cf93..48734d5fbe06 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -5263,18 +5263,16 @@ export function throwIfInfiniteUpdateLoopDetected( } } - if (__DEV__) { - if (nestedPassiveUpdateCount > NESTED_PASSIVE_UPDATE_LIMIT) { - nestedPassiveUpdateCount = 0; - rootWithPassiveNestedUpdates = null; + if (nestedPassiveUpdateCount > NESTED_PASSIVE_UPDATE_LIMIT) { + nestedPassiveUpdateCount = 0; + rootWithPassiveNestedUpdates = null; - console.error( - 'Maximum update depth exceeded. This can happen when a component ' + - "calls setState inside useEffect, but useEffect either doesn't " + - 'have a dependency array, or one of the dependencies changes on ' + - 'every render.', - ); - } + throw new Error( + 'Maximum update depth exceeded. This can happen when a component ' + + "calls setState inside useEffect, but useEffect either doesn't " + + 'have a dependency array, or one of the dependencies changes on ' + + 'every render.', + ); } } From 6423063ba22f8394c320b137ab6f6d696afd4809 Mon Sep 17 00:00:00 2001 From: harshagarwalnyu Date: Sun, 10 May 2026 18:11:18 -0400 Subject: [PATCH 2/3] test: add prod-mode throw test for useEffect infinite loop detection --- .../src/__tests__/ReactUpdates-test.js | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/packages/react-dom/src/__tests__/ReactUpdates-test.js b/packages/react-dom/src/__tests__/ReactUpdates-test.js index 503f3f5c32cb..2910bf2cde40 100644 --- a/packages/react-dom/src/__tests__/ReactUpdates-test.js +++ b/packages/react-dom/src/__tests__/ReactUpdates-test.js @@ -1975,6 +1975,34 @@ describe('ReactUpdates', () => { }); } + it('throws on deferred infinite update loop with useEffect in production', async () => { + function NonTerminating() { + const [step, setStep] = React.useState(0); + React.useEffect(function myEffect() { + setStep(x => x + 1); + }); + return step; + } + + const container = document.createElement('div'); + const errors = []; + const root = ReactDOMClient.createRoot(container, { + onUncaughtError: (error, errorInfo) => { + errors.push(error.message); + }, + }); + await act(async () => { + root.render(); + }); + + expect(errors).toEqual([ + 'Maximum update depth exceeded. This can happen when a component ' + + "calls setState inside useEffect, but useEffect either doesn't " + + 'have a dependency array, or one of the dependencies changes on ' + + 'every render.', + ]); + }); + it('prevents infinite update loop triggered by synchronous updates in useEffect', async () => { // Ignore flushSync warning spyOnDev(console, 'error').mockImplementation(() => {}); From 22daf3be9f168cb38c59d1c996a21a51fde1bcce Mon Sep 17 00:00:00 2001 From: harshagarwalnyu Date: Sun, 10 May 2026 18:19:19 -0400 Subject: [PATCH 3/3] test: fix DEV infinite-loop useEffect test to expect throw not console.error --- .../src/__tests__/ReactUpdates-test.js | 36 ++++++++----------- 1 file changed, 15 insertions(+), 21 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactUpdates-test.js b/packages/react-dom/src/__tests__/ReactUpdates-test.js index 2910bf2cde40..b1b9da034368 100644 --- a/packages/react-dom/src/__tests__/ReactUpdates-test.js +++ b/packages/react-dom/src/__tests__/ReactUpdates-test.js @@ -1884,7 +1884,7 @@ describe('ReactUpdates', () => { // TODO: Replace this branch with @gate pragmas if (__DEV__) { - it('warns about a deferred infinite update loop with useEffect', async () => { + it('throws on deferred infinite update loop with useEffect and includes owner stack', async () => { function NonTerminating() { const [step, setStep] = React.useState(0); React.useEffect(function myEffect() { @@ -1897,29 +1897,23 @@ describe('ReactUpdates', () => { return ; } - let error = null; + let caughtError = null; let ownerStack = null; - let debugStack = null; - const originalConsoleError = console.error; - console.error = e => { - error = e; - ownerStack = React.captureOwnerStack(); - debugStack = new Error().stack; - Scheduler.log('stop'); - }; - try { - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container, { + onUncaughtError: (error, errorInfo) => { + caughtError = error; + ownerStack = errorInfo.componentStack; + }, + }); + await act(async () => { root.render(); - await waitFor(['stop']); - } finally { - console.error = originalConsoleError; - } + }); - expect(error).toContain('Maximum update depth exceeded'); - // The currently executing effect should be on the native stack - expect(debugStack).toContain('at myEffect'); - expect(ownerStack).toContain('at App'); + expect(caughtError).not.toBeNull(); + expect(caughtError.message).toContain('Maximum update depth exceeded'); + // Owner stack should include the component that scheduled the looping effect + expect(ownerStack).toContain('NonTerminating'); }); it('can have nested updates if they do not cross the limit', async () => {