Skip to content

Commit 17d59fc

Browse files
committed
Remove ad hoc throw
Fatal errors (errors that are not captured by an error boundary) are currently rethrown from directly inside the render phase's `catch` block. This is a refactor hazard because the code in this branch has to mirror the code that happens at the end of the function, when exiting the render phase in the normal case. This commit moves the throw to the end, using a new root exit status.
1 parent 35ab8c0 commit 17d59fc

File tree

2 files changed

+116
-115
lines changed

2 files changed

+116
-115
lines changed

packages/react-reconciler/src/ReactFiberWorkLoop.js

Lines changed: 114 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -200,13 +200,14 @@ const LegacyUnbatchedContext = /* */ 0b001000;
200200
const RenderContext = /* */ 0b010000;
201201
const CommitContext = /* */ 0b100000;
202202

203-
type RootExitStatus = 0 | 1 | 2 | 3 | 4 | 5;
203+
type RootExitStatus = 0 | 1 | 2 | 3 | 4 | 5 | 6;
204204
const RootIncomplete = 0;
205-
const RootErrored = 1;
206-
const RootSuspended = 2;
207-
const RootSuspendedWithDelay = 3;
208-
const RootCompleted = 4;
209-
const RootLocked = 5;
205+
const RootFatalErrored = 1;
206+
const RootErrored = 2;
207+
const RootSuspended = 3;
208+
const RootSuspendedWithDelay = 4;
209+
const RootCompleted = 5;
210+
const RootLocked = 6;
210211

211212
export type Thenable = {
212213
then(resolve: () => mixed, reject?: () => mixed): Thenable | void,
@@ -225,6 +226,8 @@ let workInProgress: Fiber | null = null;
225226
let renderExpirationTime: ExpirationTime = NoWork;
226227
// Whether to root completed, errored, suspended, etc.
227228
let workInProgressRootExitStatus: RootExitStatus = RootIncomplete;
229+
// A fatal error, if one is thrown
230+
let workInProgressRootFatalError: mixed = null;
228231
// Most recent event time among processed updates during this render.
229232
// This is conceptually a time stamp but expressed in terms of an ExpirationTime
230233
// because we deal mostly with expiration times in the hot path, so this avoids
@@ -685,28 +688,7 @@ function performConcurrentWorkOnRoot(root, didTimeout) {
685688
workLoopConcurrent();
686689
break;
687690
} catch (thrownValue) {
688-
// Reset module-level state that was set during the render phase.
689-
resetContextDependencies();
690-
resetHooks();
691-
692-
if (workInProgress === null || workInProgress.return === null) {
693-
// Expected to be working on a non-root fiber. This is a fatal error
694-
// because there's no ancestor that can handle it; the root is
695-
// supposed to capture all errors that weren't caught by an error
696-
// boundary.
697-
prepareFreshStack(root, expirationTime);
698-
executionContext = prevExecutionContext;
699-
markRootSuspendedAtTime(root, expirationTime);
700-
ensureRootIsScheduled(root);
701-
throw thrownValue;
702-
}
703-
704-
workInProgress = handleError(
705-
root,
706-
workInProgress.return,
707-
workInProgress,
708-
thrownValue,
709-
);
691+
handleError(root, workInProgress, thrownValue);
710692
}
711693
} while (true);
712694
resetContextDependencies();
@@ -715,36 +697,42 @@ function performConcurrentWorkOnRoot(root, didTimeout) {
715697
if (enableSchedulerTracing) {
716698
popInteractions(((prevInteractions: any): Set<Interaction>));
717699
}
718-
}
719-
720-
if (workInProgress !== null) {
721-
// There's still work left over. Exit without committing.
722-
stopInterruptedWorkLoopTimer();
723-
} else {
724-
// We now have a consistent tree. The next step is either to commit it,
725-
// or, if something suspended, wait to commit it after a timeout.
726-
stopFinishedWorkLoopTimer();
727700

728-
const finishedWork: Fiber = ((root.finishedWork =
729-
root.current.alternate): any);
730-
root.finishedExpirationTime = expirationTime;
701+
if (workInProgressRootExitStatus === RootFatalErrored) {
702+
const fatalError = workInProgressRootFatalError;
703+
stopInterruptedWorkLoopTimer();
704+
prepareFreshStack(root, expirationTime);
705+
markRootSuspendedAtTime(root, expirationTime);
706+
ensureRootIsScheduled(root);
707+
throw fatalError;
708+
}
731709

732-
resolveLocksOnRoot(root, expirationTime);
710+
if (workInProgress !== null) {
711+
// There's still work left over. Exit without committing.
712+
stopInterruptedWorkLoopTimer();
713+
} else {
714+
// We now have a consistent tree. The next step is either to commit it,
715+
// or, if something suspended, wait to commit it after a timeout.
716+
stopFinishedWorkLoopTimer();
717+
718+
const finishedWork: Fiber = ((root.finishedWork =
719+
root.current.alternate): any);
720+
root.finishedExpirationTime = expirationTime;
721+
resolveLocksOnRoot(root, expirationTime);
722+
finishConcurrentRender(
723+
root,
724+
finishedWork,
725+
workInProgressRootExitStatus,
726+
expirationTime,
727+
);
728+
}
733729

734-
finishConcurrentRender(
735-
root,
736-
finishedWork,
737-
workInProgressRootExitStatus,
738-
expirationTime,
739-
);
740-
}
741-
// Before exiting, make sure there's a callback scheduled for the next
742-
// pending level.
743-
ensureRootIsScheduled(root);
744-
if (root.callbackNode === originalCallbackNode) {
745-
// The task node scheduled for this root is the same one that's
746-
// currently executed. Need to return a continuation.
747-
return performConcurrentWorkOnRoot.bind(null, root);
730+
ensureRootIsScheduled(root);
731+
if (root.callbackNode === originalCallbackNode) {
732+
// The task node scheduled for this root is the same one that's
733+
// currently executed. Need to return a continuation.
734+
return performConcurrentWorkOnRoot.bind(null, root);
735+
}
748736
}
749737
}
750738
return null;
@@ -760,8 +748,9 @@ function finishConcurrentRender(
760748
workInProgressRoot = null;
761749

762750
switch (exitStatus) {
763-
case RootIncomplete: {
764-
invariant(false, 'Should have a work-in-progress.');
751+
case RootIncomplete:
752+
case RootFatalErrored: {
753+
invariant(false, 'Root did not complete. This is a bug in React.');
765754
}
766755
// Flow knows about invariant, so it complains if I add a break
767756
// statement, but eslint doesn't know about invariant, so it complains
@@ -1042,28 +1031,7 @@ function performSyncWorkOnRoot(root) {
10421031
workLoopSync();
10431032
break;
10441033
} catch (thrownValue) {
1045-
// Reset module-level state that was set during the render phase.
1046-
resetContextDependencies();
1047-
resetHooks();
1048-
1049-
if (workInProgress === null || workInProgress.return === null) {
1050-
// Expected to be working on a non-root fiber. This is a fatal error
1051-
// because there's no ancestor that can handle it; the root is
1052-
// supposed to capture all errors that weren't caught by an error
1053-
// boundary.
1054-
prepareFreshStack(root, expirationTime);
1055-
executionContext = prevExecutionContext;
1056-
markRootSuspendedAtTime(root, expirationTime);
1057-
ensureRootIsScheduled(root);
1058-
throw thrownValue;
1059-
}
1060-
1061-
workInProgress = handleError(
1062-
root,
1063-
workInProgress.return,
1064-
workInProgress,
1065-
thrownValue,
1066-
);
1034+
handleError(root, workInProgress, thrownValue);
10671035
}
10681036
} while (true);
10691037
resetContextDependencies();
@@ -1072,46 +1040,63 @@ function performSyncWorkOnRoot(root) {
10721040
if (enableSchedulerTracing) {
10731041
popInteractions(((prevInteractions: any): Set<Interaction>));
10741042
}
1075-
}
10761043

1077-
invariant(
1078-
workInProgressRootExitStatus !== RootIncomplete,
1079-
'Cannot commit an incomplete root. This error is likely caused by a ' +
1080-
'bug in React. Please file an issue.',
1081-
);
1044+
if (workInProgressRootExitStatus === RootFatalErrored) {
1045+
const fatalError = workInProgressRootFatalError;
1046+
stopInterruptedWorkLoopTimer();
1047+
prepareFreshStack(root, expirationTime);
1048+
markRootSuspendedAtTime(root, expirationTime);
1049+
ensureRootIsScheduled(root);
1050+
throw fatalError;
1051+
}
10821052

1083-
// We now have a consistent tree. The next step is either to commit it,
1084-
// or, if something suspended, wait to commit it after a timeout.
1085-
stopFinishedWorkLoopTimer();
1053+
if (workInProgress !== null) {
1054+
// This is a sync render, so we should have finished the whole tree.
1055+
invariant(
1056+
false,
1057+
'Cannot commit an incomplete root. This error is likely caused by a ' +
1058+
'bug in React. Please file an issue.',
1059+
);
1060+
} else {
1061+
// We now have a consistent tree. Because this is a sync render, we
1062+
// will commit it even if something suspended. The only exception is
1063+
// if the root is locked (using the unstable_createBatch API).
1064+
stopFinishedWorkLoopTimer();
1065+
root.finishedWork = (root.current.alternate: any);
1066+
root.finishedExpirationTime = expirationTime;
1067+
resolveLocksOnRoot(root, expirationTime);
1068+
finishSyncRender(root, workInProgressRootExitStatus, expirationTime);
1069+
}
10861070

1087-
root.finishedWork = ((root.current.alternate: any): Fiber);
1088-
root.finishedExpirationTime = expirationTime;
1071+
// Before exiting, make sure there's a callback scheduled for the next
1072+
// pending level.
1073+
ensureRootIsScheduled(root);
1074+
}
1075+
}
10891076

1090-
resolveLocksOnRoot(root, expirationTime);
1091-
if (workInProgressRootExitStatus === RootLocked) {
1092-
// This root has a lock that prevents it from committing. Exit. If we
1093-
// begin work on the root again, without any intervening updates, it
1094-
// will finish without doing additional work.
1095-
markRootSuspendedAtTime(root, expirationTime);
1096-
} else {
1097-
// Set this to null to indicate there's no in-progress render.
1098-
workInProgressRoot = null;
1077+
return null;
1078+
}
10991079

1100-
if (__DEV__) {
1101-
if (
1102-
workInProgressRootExitStatus === RootSuspended ||
1103-
workInProgressRootExitStatus === RootSuspendedWithDelay
1104-
) {
1105-
flushSuspensePriorityWarningInDEV();
1106-
}
1080+
function finishSyncRender(root, exitStatus, expirationTime) {
1081+
if (exitStatus === RootLocked) {
1082+
// This root has a lock that prevents it from committing. Exit. If we
1083+
// begin work on the root again, without any intervening updates, it
1084+
// will finish without doing additional work.
1085+
markRootSuspendedAtTime(root, expirationTime);
1086+
} else {
1087+
// Set this to null to indicate there's no in-progress render.
1088+
workInProgressRoot = null;
1089+
1090+
if (__DEV__) {
1091+
if (
1092+
exitStatus === RootSuspended ||
1093+
exitStatus === RootSuspendedWithDelay
1094+
) {
1095+
flushSuspensePriorityWarningInDEV();
11071096
}
1108-
commitRoot(root);
11091097
}
1098+
commitRoot(root);
11101099
}
1111-
// Before exiting, make sure there's a callback scheduled for the next
1112-
// pending level.
1113-
ensureRootIsScheduled(root);
1114-
return null;
11151100
}
11161101

11171102
export function flushRoot(root: FiberRoot, expirationTime: ExpirationTime) {
@@ -1320,6 +1305,7 @@ function prepareFreshStack(root, expirationTime) {
13201305
workInProgress = createWorkInProgress(root.current, null, expirationTime);
13211306
renderExpirationTime = expirationTime;
13221307
workInProgressRootExitStatus = RootIncomplete;
1308+
workInProgressRootFatalError = null;
13231309
workInProgressRootLatestProcessedExpirationTime = Sync;
13241310
workInProgressRootLatestSuspenseTimeout = Sync;
13251311
workInProgressRootCanSuspendUsingConfig = null;
@@ -1336,7 +1322,21 @@ function prepareFreshStack(root, expirationTime) {
13361322
}
13371323
}
13381324

1339-
function handleError(root, returnFiber, sourceFiber, thrownValue) {
1325+
function handleError(root, sourceFiber, thrownValue) {
1326+
// Reset module-level state that was set during the render phase.
1327+
resetContextDependencies();
1328+
resetHooks();
1329+
1330+
if (workInProgress === null || workInProgress.return === null) {
1331+
// Expected to be working on a non-root fiber. This is a fatal error
1332+
// because there's no ancestor that can handle it; the root is
1333+
// supposed to capture all errors that weren't caught by an error
1334+
// boundary.
1335+
workInProgressRootExitStatus = RootFatalErrored;
1336+
workInProgressRootFatalError = thrownValue;
1337+
return null;
1338+
}
1339+
13401340
if (enableProfilerTimer && sourceFiber.mode & ProfileMode) {
13411341
// Record the time spent rendering before an error was thrown. This
13421342
// avoids inaccurate Profiler durations in the case of a
@@ -1346,14 +1346,14 @@ function handleError(root, returnFiber, sourceFiber, thrownValue) {
13461346

13471347
throwException(
13481348
root,
1349-
returnFiber,
1349+
workInProgress.return,
13501350
sourceFiber,
13511351
thrownValue,
13521352
renderExpirationTime,
13531353
);
13541354
// TODO: This is not wrapped in a try-catch, so if the complete phase
13551355
// throws, we won't capture it.
1356-
return completeUnitOfWork(sourceFiber);
1356+
workInProgress = completeUnitOfWork(sourceFiber);
13571357
}
13581358

13591359
function pushDispatcher(root) {

scripts/error-codes/codes.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -342,5 +342,6 @@
342342
"341": "We just came from a parent so we must have had a parent. This is a bug in React.",
343343
"342": "A React component suspended while rendering, but no fallback UI was specified.\n\nAdd a <Suspense fallback=...> component higher in the tree to provide a loading indicator or placeholder to display.",
344344
"343": "ReactDOMServer does not yet support scope components.",
345-
"344": "Expected prepareToHydrateHostSuspenseInstance() to never be called. This error is likely caused by a bug in React. Please file an issue."
345+
"344": "Expected prepareToHydrateHostSuspenseInstance() to never be called. This error is likely caused by a bug in React. Please file an issue.",
346+
"345": "Root did not complete. This is a bug in React."
346347
}

0 commit comments

Comments
 (0)