diff --git a/packages/react-reconciler/src/__tests__/ReactIncrementalErrorLogging-test.js b/packages/react-reconciler/src/__tests__/ReactIncrementalErrorLogging-test.js index 3dde9c75bf03..f759fc60ef35 100644 --- a/packages/react-reconciler/src/__tests__/ReactIncrementalErrorLogging-test.js +++ b/packages/react-reconciler/src/__tests__/ReactIncrementalErrorLogging-test.js @@ -213,6 +213,67 @@ describe('ReactIncrementalErrorLogging', () => { }).toThrow('logCapturedError error'); }); + 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. + // 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}, + ); + }); + 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.', + ]); + } + }); + + 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.', + ]); + } + }); + it('resets instance variables before unmounting failed node', async () => { class ErrorBoundary extends React.Component { state = {error: null}; diff --git a/packages/react-reconciler/src/getComponentNameFromFiber.js b/packages/react-reconciler/src/getComponentNameFromFiber.js index 97124bbf5ba5..8719539cfc08 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 !== null) { + return getComponentNameFromFiber(fiber.return); + } + return null; case Profiler: return 'Profiler'; case ScopeComponent: