From 178829952f4b2b42dd8dceef875894d316752d3e Mon Sep 17 00:00:00 2001 From: Rick Hanlon Date: Wed, 11 Feb 2026 09:45:43 -0500 Subject: [PATCH 1/3] add failing test --- .../ReactIncrementalErrorLogging-test.js | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/packages/react-reconciler/src/__tests__/ReactIncrementalErrorLogging-test.js b/packages/react-reconciler/src/__tests__/ReactIncrementalErrorLogging-test.js index 3dde9c75bf03..527a706fd0ac 100644 --- a/packages/react-reconciler/src/__tests__/ReactIncrementalErrorLogging-test.js +++ b/packages/react-reconciler/src/__tests__/ReactIncrementalErrorLogging-test.js @@ -213,6 +213,44 @@ describe('ReactIncrementalErrorLogging', () => { }).toThrow('logCapturedError error'); }); + it('reports Offscreen as the source component for errors thrown during reconciliation inside Suspense', async () => { + // When a child of Suspense throws during reconciliation (not render), + // a Throw fiber is created whose .return is the internal Offscreen fiber. + // getComponentNameFromFiber falls through to fiber.return for Throw fibers, + // which returns 'Offscreen' — an internal implementation detail that + // shouldn't be shown to users. + // React.lazy used as a direct child value (not a JSX element) so the + // REACT_LAZY_TYPE case in reconcileChildFibersImpl is hit and + // resolveLazy throws during reconciliation, creating a Throw fiber. + const lazyChild = React.lazy(() => { + throw new Error('lazy init error'); + }); + await fakeAct(() => { + ReactNoop.render( + }>{lazyChild}, + ); + }); + expect(uncaughtExceptionMock).toHaveBeenCalledTimes(1); + expect(uncaughtExceptionMock).toHaveBeenCalledWith( + expect.objectContaining({ + message: 'lazy init error', + }), + ); + if (__DEV__) { + expect(console.warn).toHaveBeenCalledTimes(1); + expect(console.warn).toHaveBeenCalledWith( + expect.stringContaining('%s'), + expect.stringContaining( + 'An error occurred in the component.', + ), + expect.stringContaining( + 'Consider adding an error boundary to your tree ' + + 'to customize error handling behavior.', + ), + ); + } + }); + it('resets instance variables before unmounting failed node', async () => { class ErrorBoundary extends React.Component { state = {error: null}; From a08a5a9f527aff0d7766dd4075cd0e2e80910cdc Mon Sep 17 00:00:00 2001 From: Rick Hanlon Date: Wed, 11 Feb 2026 10:17:50 -0500 Subject: [PATCH 2/3] Fix - don't show in error message. --- .../ReactIncrementalErrorLogging-test.js | 55 +++++++++++++------ .../src/getComponentNameFromFiber.js | 5 +- 2 files changed, 43 insertions(+), 17 deletions(-) diff --git a/packages/react-reconciler/src/__tests__/ReactIncrementalErrorLogging-test.js b/packages/react-reconciler/src/__tests__/ReactIncrementalErrorLogging-test.js index 527a706fd0ac..f759fc60ef35 100644 --- a/packages/react-reconciler/src/__tests__/ReactIncrementalErrorLogging-test.js +++ b/packages/react-reconciler/src/__tests__/ReactIncrementalErrorLogging-test.js @@ -213,18 +213,15 @@ describe('ReactIncrementalErrorLogging', () => { }).toThrow('logCapturedError error'); }); - it('reports Offscreen as the source component for errors thrown during reconciliation inside Suspense', async () => { + it('does not report internal Offscreen component for errors thrown during reconciliation inside Suspense', async () => { // When a child of Suspense throws during reconciliation (not render), // a Throw fiber is created whose .return is the internal Offscreen fiber. - // getComponentNameFromFiber falls through to fiber.return for Throw fibers, - // which returns 'Offscreen' — an internal implementation detail that - // shouldn't be shown to users. - // React.lazy used as a direct child value (not a JSX element) so the - // REACT_LAZY_TYPE case in reconcileChildFibersImpl is hit and - // resolveLazy throws during reconciliation, creating a Throw fiber. + // We should skip Offscreen since it's an internal + // implementation detail and walk up to Suspense instead. const lazyChild = React.lazy(() => { throw new Error('lazy init error'); }); + await fakeAct(() => { ReactNoop.render( }>{lazyChild}, @@ -238,16 +235,42 @@ describe('ReactIncrementalErrorLogging', () => { ); if (__DEV__) { expect(console.warn).toHaveBeenCalledTimes(1); - expect(console.warn).toHaveBeenCalledWith( - expect.stringContaining('%s'), - expect.stringContaining( - 'An error occurred in the component.', - ), - expect.stringContaining( - 'Consider adding an error boundary to your tree ' + - 'to customize error handling behavior.', - ), + expect(console.warn.mock.calls[0]).toEqual([ + '%s\n\n%s\n', + 'An error occurred in the component.', + 'Consider adding an error boundary to your tree to customize error handling behavior.\n' + + 'Visit https://react.dev/link/error-boundaries to learn more about error boundaries.', + ]); + } + }); + + it('does not report internal Offscreen component for errors thrown during reconciliation inside Activity', async () => { + // Same as the Suspense test above — Activity also wraps its children in + // an internal Offscreen fiber. The error message should show Activity, + // not Offscreen. + const lazyChild = React.lazy(() => { + throw new Error('lazy init error'); + }); + + await fakeAct(() => { + ReactNoop.render( + {lazyChild}, ); + }); + expect(uncaughtExceptionMock).toHaveBeenCalledTimes(1); + expect(uncaughtExceptionMock).toHaveBeenCalledWith( + expect.objectContaining({ + message: 'lazy init error', + }), + ); + if (__DEV__) { + expect(console.warn).toHaveBeenCalledTimes(1); + expect(console.warn.mock.calls[0]).toEqual([ + '%s\n\n%s\n', + 'An error occurred in the component.', + 'Consider adding an error boundary to your tree to customize error handling behavior.\n' + + 'Visit https://react.dev/link/error-boundaries to learn more about error boundaries.', + ]); } }); diff --git a/packages/react-reconciler/src/getComponentNameFromFiber.js b/packages/react-reconciler/src/getComponentNameFromFiber.js index 97124bbf5ba5..84ed920447bf 100644 --- a/packages/react-reconciler/src/getComponentNameFromFiber.js +++ b/packages/react-reconciler/src/getComponentNameFromFiber.js @@ -122,7 +122,10 @@ export default function getComponentNameFromFiber(fiber: Fiber): string | null { } return 'Mode'; case OffscreenComponent: - return 'Offscreen'; + if (fiber.return) { + return getComponentNameFromFiber(fiber.return); + } + return null; case Profiler: return 'Profiler'; case ScopeComponent: From 80ac49a34bd6980560dadb4b239912948533cf71 Mon Sep 17 00:00:00 2001 From: Rick Hanlon Date: Wed, 11 Feb 2026 10:38:13 -0500 Subject: [PATCH 3/3] !== null --- packages/react-reconciler/src/getComponentNameFromFiber.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-reconciler/src/getComponentNameFromFiber.js b/packages/react-reconciler/src/getComponentNameFromFiber.js index 84ed920447bf..8719539cfc08 100644 --- a/packages/react-reconciler/src/getComponentNameFromFiber.js +++ b/packages/react-reconciler/src/getComponentNameFromFiber.js @@ -122,7 +122,7 @@ export default function getComponentNameFromFiber(fiber: Fiber): string | null { } return 'Mode'; case OffscreenComponent: - if (fiber.return) { + if (fiber.return !== null) { return getComponentNameFromFiber(fiber.return); } return null;