Recover from errors with a boundary in completion phase (#14104)
* Recover from errors with a boundary in completion phase * Use a separate field for completing unit of work * Use a simpler fix with one boolean * Reoder conditions * Clarify which paths are DEV-only * Move duplicated line out * Make it clearer this code is DEV-only
Dan Abramov committed
Nov 6, 2018 at 23:38 UTC
3c69a1881421d46d738e2782fe3ec0ae9b5cb39c
2 files changed
+39
-4
packages/react-dom/src/__tests__/ReactErrorBoundaries-test.internal.js
+15
@@ -2149,4 +2149,19 @@ describe('ReactErrorBoundaries', () => {
2149
expect(componentDidCatchError).toBe(thrownError);
2150
expect(getDerivedStateFromErrorError).toBe(thrownError);
2151
});
2152
+
2153
+ it('should catch errors from invariants in completion phase', () => {
2154
+ const container = document.createElement('div');
2155
+ ReactDOM.render(
2156
+ <ErrorBoundary>
2157
+ <input>
2158
+ <div />
2159
+ </input>
2160
+ </ErrorBoundary>,
2161
+ container,
2162
+ );
2163
+ expect(container.textContent).toContain(
2164
+ 'Caught an error: input is a void element tag',
2165
+ );
2166
+ });
2167
});
packages/react-reconciler/src/ReactFiberScheduler.js
+24
-4
@@ -275,11 +275,13 @@ let interruptedBy: Fiber | null = null;
275
276
let stashedWorkInProgressProperties;
277
let replayUnitOfWork;
278
+let mayReplayFailedUnitOfWork;
279
let isReplayingFailedUnitOfWork;
280
let originalReplayError;
281
let rethrowOriginalError;
282
if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) {
283
stashedWorkInProgressProperties = null;
284
+ mayReplayFailedUnitOfWork = true;
285
isReplayingFailedUnitOfWork = false;
286
originalReplayError = null;
287
replayUnitOfWork = (
@@ -952,18 +954,22 @@ function completeUnitOfWork(workInProgress: Fiber): Fiber | null {
954
const siblingFiber = workInProgress.sibling;
955
956
if ((workInProgress.effectTag & Incomplete) === NoEffect) {
957
+ if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) {
958
+ // Don't replay if it fails during completion phase.
959
+ mayReplayFailedUnitOfWork = false;
960
+ }
961
// This fiber completed.
962
+ // Remember we're completing this unit so we can find a boundary if it fails.
963
+ nextUnitOfWork = workInProgress;
964
if (enableProfilerTimer) {
965
if (workInProgress.mode & ProfileMode) {
966
startProfilerTimer(workInProgress);
967
}
960
-
968
nextUnitOfWork = completeWork(
969
current,
970
workInProgress,
971
nextRenderExpirationTime,
972
);
966
-
973
if (workInProgress.mode & ProfileMode) {
974
// Update render duration assuming we didn't error.
975
stopProfilerTimerIfRunningAndRecordDelta(workInProgress, false);
@@ -975,6 +981,10 @@ function completeUnitOfWork(workInProgress: Fiber): Fiber | null {
981
nextRenderExpirationTime,
982
);
983
}
984
+ if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) {
985
+ // We're out of completion phase so replaying is fine now.
986
+ mayReplayFailedUnitOfWork = true;
987
+ }
988
stopWorkTimer(workInProgress);
989
resetChildExpirationTime(workInProgress, nextRenderExpirationTime);
990
if (__DEV__) {
@@ -1292,6 +1302,14 @@ function renderRoot(root: FiberRoot, isYieldy: boolean): void {
1302
resetContextDependences();
1303
resetHooks();
1304
1305
+ // Reset in case completion throws.
1306
+ // This is only used in DEV and when replaying is on.
1307
+ let mayReplay;
1308
+ if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) {
1309
+ mayReplay = mayReplayFailedUnitOfWork;
1310
+ mayReplayFailedUnitOfWork = true;
1311
+ }
1312
+
1313
if (nextUnitOfWork === null) {
1314
// This is a fatal error.
1315
didFatal = true;
@@ -1303,9 +1321,11 @@ function renderRoot(root: FiberRoot, isYieldy: boolean): void {
1321
(resetCurrentlyProcessingQueue: any)();
1322
}
1323
1306
- const failedUnitOfWork: Fiber = nextUnitOfWork;
1324
if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) {
1308
- replayUnitOfWork(failedUnitOfWork, thrownValue, isYieldy);
1325
+ if (mayReplay) {
1326
+ const failedUnitOfWork: Fiber = nextUnitOfWork;
1327
+ replayUnitOfWork(failedUnitOfWork, thrownValue, isYieldy);
1328
+ }
1329
}
1330
1331
// TODO: we already know this isn't true in some cases.