Skip to content

Commit a5a0366

Browse files
committed
Re-throw root errors thrown in complete phase
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.) The only way it can happen is if the renderer's host config throws. It turns out we had a test case for this when the error is thrown in the begin phase, but not the complete phase. This adds a test case for the complete phase 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 now, I'm treating it like an internal invariant and immediately exiting.
1 parent 58b8797 commit a5a0366

File tree

3 files changed

+21
-1
lines changed

3 files changed

+21
-1
lines changed

packages/react-noop-renderer/src/createReactNoop.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,13 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
533533
newChildren: Array<Instance | TextInstance>,
534534
): void {
535535
container.pendingChildren = newChildren;
536+
if (
537+
newChildren.length === 1 &&
538+
newChildren[0].text === 'Error when completing root'
539+
) {
540+
// Trigger an error for testing purposes
541+
throw Error('Error when completing root');
542+
}
536543
},
537544

538545
replaceContainerChildren(

packages/react-reconciler/src/ReactFiberWorkLoop.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1285,6 +1285,7 @@ function handleError(root, thrownValue) {
12851285
// boundary.
12861286
workInProgressRootExitStatus = RootFatalErrored;
12871287
workInProgressRootFatalError = thrownValue;
1288+
workInProgress = null;
12881289
return null;
12891290
}
12901291

packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1249,7 +1249,7 @@ describe('ReactIncrementalErrorHandling', () => {
12491249
]);
12501250
});
12511251

1252-
it('recovers from uncaught reconciler errors', () => {
1252+
it('recovers from error at the root (begin phase)', () => {
12531253
const InvalidType = undefined;
12541254
expect(() =>
12551255
ReactNoop.render(<InvalidType />),
@@ -1271,6 +1271,18 @@ describe('ReactIncrementalErrorHandling', () => {
12711271
expect(ReactNoop.getChildren()).toEqual([span('hi')]);
12721272
});
12731273

1274+
if (global.__PERSISTENT__) {
1275+
it('recovers from error at the root (complete phase)', () => {
1276+
const root = ReactNoop.createRoot();
1277+
root.render('Error when completing root');
1278+
expect(Scheduler).toFlushAndThrow('Error when completing root');
1279+
1280+
const blockingRoot = ReactNoop.createBlockingRoot();
1281+
blockingRoot.render('Error when completing root');
1282+
expect(Scheduler).toFlushAndThrow('Error when completing root');
1283+
});
1284+
}
1285+
12741286
it('unmounts components with uncaught errors', () => {
12751287
const ops = [];
12761288
let inst;

0 commit comments

Comments
 (0)