From edab5c074fe6e9331ee64ca087e9ddeea7f56a3d Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Tue, 18 Feb 2020 15:55:38 -0800 Subject: [PATCH] Re-throw errors thrown by the renderer at the root in the complete phase (#18029) * Re-throw errors thrown by the renderer at the root React treats errors thrown at the root as a fatal because there's no parent component that can capture it. (This is distinct from an "uncaught error" that isn't wrapped in an error boundary, because in that case we can fall back to deleting the whole tree -- not great, but at least the error is contained to a single root, and React is left in a consistent state.) It turns out we didn't have a test case for this path. The only way it can happen is if the renderer's host config throws. We had similar test cases for host components, but none for the host root. This adds a new test case and fixes a bug where React would keep retrying the root because the `workInProgress` pointer was not advanced to the next fiber. (Which in this case is `null`, since it's the root.) We could consider in the future trying to gracefully exit from certain types of root errors without leaving React in an inconsistent state. For example, we should be able to gracefully exit from errors thrown in the begin phase. For now, I'm treating it like an internal invariant and immediately exiting. * Add comment --- packages/react-noop-renderer/src/createReactNoop.js | 7 +++++++ packages/react-reconciler/src/ReactFiberWorkLoop.js | 7 +++++++ .../ReactIncrementalErrorHandling-test.internal.js | 12 ++++++++++++ 3 files changed, 26 insertions(+) diff --git a/packages/react-noop-renderer/src/createReactNoop.js b/packages/react-noop-renderer/src/createReactNoop.js index 58cc128685d5e..a452a391f28d5 100644 --- a/packages/react-noop-renderer/src/createReactNoop.js +++ b/packages/react-noop-renderer/src/createReactNoop.js @@ -534,6 +534,13 @@ function createReactNoop(reconciler: Function, useMutation: boolean) { newChildren: Array, ): void { container.pendingChildren = newChildren; + if ( + newChildren.length === 1 && + newChildren[0].text === 'Error when completing root' + ) { + // Trigger an error for testing purposes + throw Error('Error when completing root'); + } }, replaceContainerChildren( diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index a4664613ac224..5d92eb8ad3833 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -1291,6 +1291,13 @@ function handleError(root, thrownValue) { // boundary. workInProgressRootExitStatus = RootFatalErrored; workInProgressRootFatalError = thrownValue; + // Set `workInProgress` to null. This represents advancing to the next + // sibling, or the parent if there are no siblings. But since the root + // has no siblings nor a parent, we set it to null. Usually this is + // handled by `completeUnitOfWork` or `unwindWork`, but since we're + // interntionally not calling those, we need set it here. + // TODO: Consider calling `unwindWork` to pop the contexts. + workInProgress = null; return null; } diff --git a/packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js b/packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js index bcb74b6ea07f8..6bea104b78db5 100644 --- a/packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js @@ -1658,4 +1658,16 @@ describe('ReactIncrementalErrorHandling', () => { 'Please update the following components: Provider', ]); }); + + if (global.__PERSISTENT__) { + it('regression test: should fatal if error is thrown at the root', () => { + const root = ReactNoop.createRoot(); + root.render('Error when completing root'); + expect(Scheduler).toFlushAndThrow('Error when completing root'); + + const blockingRoot = ReactNoop.createBlockingRoot(); + blockingRoot.render('Error when completing root'); + expect(Scheduler).toFlushAndThrow('Error when completing root'); + }); + } });