Skip to content

Commit

Permalink
Re-throw errors thrown by the renderer at the root in the complete ph…
Browse files Browse the repository at this point in the history
…ase (#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
  • Loading branch information
acdlite authored Feb 18, 2020
1 parent 14afeb1 commit 4d9f850
Show file tree
Hide file tree
Showing 3 changed files with 26 additions and 0 deletions.
7 changes: 7 additions & 0 deletions packages/react-noop-renderer/src/createReactNoop.js
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,13 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
newChildren: Array<Instance | TextInstance>,
): 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(
Expand Down
7 changes: 7 additions & 0 deletions packages/react-reconciler/src/ReactFiberWorkLoop.js
Original file line number Diff line number Diff line change
Expand Up @@ -1285,6 +1285,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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
}
});

0 comments on commit 4d9f850

Please sign in to comment.