diff --git a/packages/react-dom/src/__tests__/ReactErrorBoundaries-test.internal.js b/packages/react-dom/src/__tests__/ReactErrorBoundaries-test.internal.js index f85fc1fa13eac..4b7f5f3f6c5fd 100644 --- a/packages/react-dom/src/__tests__/ReactErrorBoundaries-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactErrorBoundaries-test.internal.js @@ -2149,4 +2149,19 @@ describe('ReactErrorBoundaries', () => { expect(componentDidCatchError).toBe(thrownError); expect(getDerivedStateFromErrorError).toBe(thrownError); }); + + it('should catch errors from invariants in completion phase', () => { + const container = document.createElement('div'); + ReactDOM.render( + + +
+ + , + container, + ); + expect(container.textContent).toContain( + 'Caught an error: input is a void element tag', + ); + }); }); diff --git a/packages/react-reconciler/src/ReactFiberScheduler.js b/packages/react-reconciler/src/ReactFiberScheduler.js index 866fa9cdcc655..bccffa0bf6444 100644 --- a/packages/react-reconciler/src/ReactFiberScheduler.js +++ b/packages/react-reconciler/src/ReactFiberScheduler.js @@ -274,11 +274,13 @@ let interruptedBy: Fiber | null = null; let stashedWorkInProgressProperties; let replayUnitOfWork; +let mayReplayFailedUnitOfWork; let isReplayingFailedUnitOfWork; let originalReplayError; let rethrowOriginalError; if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) { stashedWorkInProgressProperties = null; + mayReplayFailedUnitOfWork = true; isReplayingFailedUnitOfWork = false; originalReplayError = null; replayUnitOfWork = ( @@ -947,18 +949,22 @@ function completeUnitOfWork(workInProgress: Fiber): Fiber | null { const siblingFiber = workInProgress.sibling; if ((workInProgress.effectTag & Incomplete) === NoEffect) { + if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) { + // Don't replay if it fails during completion phase. + mayReplayFailedUnitOfWork = false; + } // This fiber completed. + // Remember we're completing this unit so we can find a boundary if it fails. + nextUnitOfWork = workInProgress; if (enableProfilerTimer) { if (workInProgress.mode & ProfileMode) { startProfilerTimer(workInProgress); } - nextUnitOfWork = completeWork( current, workInProgress, nextRenderExpirationTime, ); - if (workInProgress.mode & ProfileMode) { // Update render duration assuming we didn't error. stopProfilerTimerIfRunningAndRecordDelta(workInProgress, false); @@ -970,6 +976,10 @@ function completeUnitOfWork(workInProgress: Fiber): Fiber | null { nextRenderExpirationTime, ); } + if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) { + // We're out of completion phase so replaying is fine now. + mayReplayFailedUnitOfWork = true; + } stopWorkTimer(workInProgress); resetChildExpirationTime(workInProgress, nextRenderExpirationTime); if (__DEV__) { @@ -1277,6 +1287,14 @@ function renderRoot(root: FiberRoot, isYieldy: boolean): void { resetContextDependences(); resetHooks(); + // Reset in case completion throws. + // This is only used in DEV and when replaying is on. + let mayReplay; + if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) { + mayReplay = mayReplayFailedUnitOfWork; + mayReplayFailedUnitOfWork = true; + } + if (nextUnitOfWork === null) { // This is a fatal error. didFatal = true; @@ -1288,9 +1306,11 @@ function renderRoot(root: FiberRoot, isYieldy: boolean): void { (resetCurrentlyProcessingQueue: any)(); } - const failedUnitOfWork: Fiber = nextUnitOfWork; if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) { - replayUnitOfWork(failedUnitOfWork, thrownValue, isYieldy); + if (mayReplay) { + const failedUnitOfWork: Fiber = nextUnitOfWork; + replayUnitOfWork(failedUnitOfWork, thrownValue, isYieldy); + } } // TODO: we already know this isn't true in some cases.