@samitouri / QOS-React-2 / commits / 50bdd75a60

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.