Bubble errors if processing the error itself errors (#18567)
If we've tried completing an incomplete boundary once and failed, we don't need to try again.
Sebastian Markbåge committed
Apr 9, 2020 at 17:40 UTC
50bdd75a601d8adaf5caf34712164d8884eeb68a
3 files changed
+97
-12
packages/react-dom/src/__tests__/ReactErrorBoundaries-test.internal.js
+67
@@ -2408,4 +2408,71 @@ describe('ReactErrorBoundaries', () => {
2408
'Caught an error: input is a void element tag',
2409
);
2410
});
2411
+
2412
+ it('should catch errors from errors in the throw phase from boundaries', () => {
2413
+ const container = document.createElement('div');
2414
+
2415
+ const thrownError = new Error('original error');
2416
+ const Throws = () => {
2417
+ throw thrownError;
2418
+ };
2419
+
2420
+ class EvilErrorBoundary extends React.Component {
2421
+ get componentDidCatch() {
2422
+ throw new Error('gotta catch em all');
2423
+ }
2424
+ render() {
2425
+ return this.props.children;
2426
+ }
2427
+ }
2428
+
2429
+ ReactDOM.render(
2430
+ <ErrorBoundary>
2431
+ <EvilErrorBoundary>
2432
+ <Throws />
2433
+ </EvilErrorBoundary>
2434
+ </ErrorBoundary>,
2435
+ container,
2436
+ );
2437
+
2438
+ expect(container.textContent).toContain(
2439
+ 'Caught an error: gotta catch em all',
2440
+ );
2441
+ });
2442
+
2443
+ it('should catch errors from errors in the throw phase from errors', () => {
2444
+ const container = document.createElement('div');
2445
+
2446
+ const evilError = {
2447
+ get message() {
2448
+ throw new Error('gotta catch em all');
2449
+ },
2450
+ get stack() {
2451
+ throw new Error('gotta catch em all');
2452
+ },
2453
+ };
2454
+ const Throws = () => {
2455
+ throw evilError;
2456
+ };
2457
+ Object.defineProperty(Throws, 'displayName', {
2458
+ get: function() {
2459
+ throw new Error('gotta catch em all');
2460
+ },
2461
+ });
2462
+
2463
+ function Wrapper() {
2464
+ return <Throws />;
2465
+ }
2466
+
2467
+ ReactDOM.render(
2468
+ <ErrorBoundary>
2469
+ <Wrapper />
2470
+ </ErrorBoundary>,
2471
+ container,
2472
+ );
2473
+
2474
+ expect(container.textContent).toContain(
2475
+ 'Caught an error: gotta catch em all.',
2476
+ );
2477
+ });
2478
});
packages/react-reconciler/src/ReactFiberWorkLoop.new.js
+15
-6
@@ -1202,6 +1202,7 @@ function prepareFreshStack(root, expirationTime) {
1202
1203
function handleError(root, thrownValue): void {
1204
do {
1205
+ let erroredWork = workInProgress;
1206
try {
1207
// Reset module-level state that was set during the render phase.
1208
resetContextDependencies();
@@ -1211,7 +1212,7 @@ function handleError(root, thrownValue): void {
1212
// separate issue. Write a regression test using string refs.
1213
ReactCurrentOwner.current = null;
1214
1214
- if (workInProgress === null || workInProgress.return === null) {
1215
+ if (erroredWork === null || erroredWork.return === null) {
1216
// Expected to be working on a non-root fiber. This is a fatal error
1217
// because there's no ancestor that can handle it; the root is
1218
// supposed to capture all errors that weren't caught by an error
@@ -1228,24 +1229,32 @@ function handleError(root, thrownValue): void {
1229
return;
1230
}
1231
1231
- if (enableProfilerTimer && workInProgress.mode & ProfileMode) {
1232
+ if (enableProfilerTimer && erroredWork.mode & ProfileMode) {
1233
// Record the time spent rendering before an error was thrown. This
1234
// avoids inaccurate Profiler durations in the case of a
1235
// suspended render.
1235
- stopProfilerTimerIfRunningAndRecordDelta(workInProgress, true);
1236
+ stopProfilerTimerIfRunningAndRecordDelta(erroredWork, true);
1237
}
1238
1239
throwException(
1240
root,
1240
- workInProgress.return,
1241
- workInProgress,
1241
+ erroredWork.return,
1242
+ erroredWork,
1243
thrownValue,
1244
renderExpirationTime,
1245
);
1245
- completeUnitOfWork(workInProgress);
1246
+ completeUnitOfWork(erroredWork);
1247
} catch (yetAnotherThrownValue) {
1248
// Something in the return path also threw.
1249
thrownValue = yetAnotherThrownValue;
1250
+ if (workInProgress === erroredWork && erroredWork !== null) {
1251
+ // If this boundary has already errored, then we had trouble processing
1252
+ // the error. Bubble it to the next boundary.
1253
+ erroredWork = erroredWork.return;
1254
+ workInProgress = erroredWork;
1255
+ } else {
1256
+ erroredWork = workInProgress;
1257
+ }
1258
continue;
1259
}
1260
// Return to the normal work loop.
packages/react-reconciler/src/ReactFiberWorkLoop.old.js
+15
-6
@@ -1210,6 +1210,7 @@ function prepareFreshStack(root, expirationTime) {
1210
1211
function handleError(root, thrownValue): void {
1212
do {
1213
+ let erroredWork = workInProgress;
1214
try {
1215
// Reset module-level state that was set during the render phase.
1216
resetContextDependencies();
@@ -1219,7 +1220,7 @@ function handleError(root, thrownValue): void {
1220
// separate issue. Write a regression test using string refs.
1221
ReactCurrentOwner.current = null;
1222
1222
- if (workInProgress === null || workInProgress.return === null) {
1223
+ if (erroredWork === null || erroredWork.return === null) {
1224
// Expected to be working on a non-root fiber. This is a fatal error
1225
// because there's no ancestor that can handle it; the root is
1226
// supposed to capture all errors that weren't caught by an error
@@ -1236,24 +1237,32 @@ function handleError(root, thrownValue): void {
1237
return;
1238
}
1239
1239
- if (enableProfilerTimer && workInProgress.mode & ProfileMode) {
1240
+ if (enableProfilerTimer && erroredWork.mode & ProfileMode) {
1241
// Record the time spent rendering before an error was thrown. This
1242
// avoids inaccurate Profiler durations in the case of a
1243
// suspended render.
1243
- stopProfilerTimerIfRunningAndRecordDelta(workInProgress, true);
1244
+ stopProfilerTimerIfRunningAndRecordDelta(erroredWork, true);
1245
}
1246
1247
throwException(
1248
root,
1248
- workInProgress.return,
1249
- workInProgress,
1249
+ erroredWork.return,
1250
+ erroredWork,
1251
thrownValue,
1252
renderExpirationTime,
1253
);
1253
- completeUnitOfWork(workInProgress);
1254
+ completeUnitOfWork(erroredWork);
1255
} catch (yetAnotherThrownValue) {
1256
// Something in the return path also threw.
1257
thrownValue = yetAnotherThrownValue;
1258
+ if (workInProgress === erroredWork && erroredWork !== null) {
1259
+ // If this boundary has already errored, then we had trouble processing
1260
+ // the error. Bubble it to the next boundary.
1261
+ erroredWork = erroredWork.return;
1262
+ workInProgress = erroredWork;
1263
+ } else {
1264
+ erroredWork = workInProgress;
1265
+ }
1266
continue;
1267
}
1268
// Return to the normal work loop.