@samitouri / QOS-React / commits / 9bd4d1fae2

Synchronously restart when an error is thrown during async rendering (#13041)

In async mode, events are interleaved with rendering. If one of those events mutates state that is later accessed during render, it can lead to inconsistencies/tearing. Restarting the render from the root is often sufficient to fix the inconsistency. We'll flush the restart synchronously to prevent yet another mutation from happening during an interleaved event. We'll only restart during an async render. Sync renders are already sync, so there's no benefit in restarting. (Unless a mutation happens during the render phase, but we don't support that.)

Andrew Clark committed Jun 14, 2018 at 16:37 UTC 9bd4d1fae21a6521c185cb114a15ca5dc74d6d9b
9 files changed +219 -126
packages/react-reconciler/src/ReactFiberPendingPriority.js
+34 -1
@@ -10,7 +10,7 @@
10 import type {FiberRoot} from './ReactFiberRoot';
11 import type {ExpirationTime} from './ReactFiberExpirationTime';
12
13 -import {NoWork} from './ReactFiberExpirationTime';
13 +import {NoWork, Sync} from './ReactFiberExpirationTime';
14
15 // TODO: Offscreen updates
16
@@ -18,6 +18,11 @@ export function markPendingPriorityLevel(
18 root: FiberRoot,
19 expirationTime: ExpirationTime,
20 ): void {
21 + // If there's a gap between completing a failed root and retrying it,
22 + // additional updates may be scheduled. Clear `didError`, in case the update
23 + // is sufficient to fix the error.
24 + root.didError = false;
25 +
26 // Update the latest and earliest pending times
27 const earliestPendingTime = root.earliestPendingTime;
28 if (earliestPendingTime === NoWork) {
@@ -43,6 +48,8 @@ export function markCommittedPriorityLevels(
48 currentTime: ExpirationTime,
49 earliestRemainingTime: ExpirationTime,
50 ): void {
51 + root.didError = false;
52 +
53 if (earliestRemainingTime === NoWork) {
54 // Fast path. There's no remaining work. Clear everything.
55 root.earliestPendingTime = NoWork;
@@ -111,10 +118,30 @@ export function markCommittedPriorityLevels(
118 findNextPendingPriorityLevel(root);
119 }
120
121 +export function hasLowerPriorityWork(
122 + root: FiberRoot,
123 + renderExpirationTime: ExpirationTime,
124 +) {
125 + return (
126 + renderExpirationTime !== root.latestPendingTime &&
127 + renderExpirationTime !== root.latestSuspendedTime
128 + );
129 +}
130 +
131 export function markSuspendedPriorityLevel(
132 root: FiberRoot,
133 suspendedTime: ExpirationTime,
134 + didError: boolean,
135 ): void {
136 + if (didError && !hasLowerPriorityWork(root, suspendedTime)) {
137 + // TODO: When we add back resuming, we need to ensure the progressed work
138 + // is thrown out and not reused during the restarted render. One way to
139 + // invalidate the progressed work is to restart at expirationTime + 1.
140 + root.didError = true;
141 + findNextPendingPriorityLevel(root);
142 + return;
143 + }
144 +
145 // First, check the known pending levels and update them if needed.
146 const earliestPendingTime = root.earliestPendingTime;
147 const latestPendingTime = root.latestPendingTime;
@@ -191,6 +218,12 @@ function findNextPendingPriorityLevel(root) {
218 // nothing to work on.
219 nextExpirationTimeToWorkOn = expirationTime = root.latestPingedTime;
220 }
221 +
222 + if (root.didError) {
223 + // Revert to synchronous mode.
224 + expirationTime = Sync;
225 + }
226 +
227 root.nextExpirationTimeToWorkOn = nextExpirationTimeToWorkOn;
228 root.expirationTime = expirationTime;
229 }
packages/react-reconciler/src/ReactFiberRoot.js
+7
@@ -44,6 +44,11 @@ export type FiberRoot = {
44 // be retried.
45 latestPingedTime: ExpirationTime,
46
47 + // If an error is thrown, and there are no more updates in the queue, we try
48 + // rendering from the root one more time, synchronously, before handling
49 + // the error.
50 + didError: boolean,
51 +
52 pendingCommitExpirationTime: ExpirationTime,
53 // A finished work-in-progress HostRoot that's ready to be committed.
54 // TODO: The reason this is separate from isReadyForCommit is because the
@@ -86,6 +91,8 @@ export function createFiberRoot(
91 latestSuspendedTime: NoWork,
92 latestPingedTime: NoWork,
93
94 + didError: false,
95 +
96 pendingCommitExpirationTime: NoWork,
97 finishedWork: null,
98 context: null,
packages/react-reconciler/src/ReactFiberScheduler.js
+9 -1
@@ -229,6 +229,7 @@ let nextRoot: FiberRoot | null = null;
229 // The time at which we're currently rendering work.
230 let nextRenderExpirationTime: ExpirationTime = NoWork;
231 let nextLatestTimeoutMs: number = -1;
232 +let nextRenderDidError: boolean = false;
233
234 // The next fiber with an effect that we're currently committing.
235 let nextEffect: Fiber | null = null;
@@ -343,6 +344,7 @@ function resetStack() {
344 nextRoot = null;
345 nextRenderExpirationTime = NoWork;
346 nextLatestTimeoutMs = -1;
347 + nextRenderDidError = false;
348 nextUnitOfWork = null;
349 }
350
@@ -1007,6 +1009,7 @@ function renderRoot(root: FiberRoot, isYieldy: boolean): void {
1009 nextRoot = root;
1010 nextRenderExpirationTime = expirationTime;
1011 nextLatestTimeoutMs = -1;
1012 + nextRenderDidError = false;
1013 nextUnitOfWork = createWorkInProgress(
1014 nextRoot.current,
1015 null,
@@ -1113,7 +1116,7 @@ function renderRoot(root: FiberRoot, isYieldy: boolean): void {
1116 const didCompleteRoot = false;
1117 stopWorkLoopTimer(interruptedBy, didCompleteRoot);
1118 interruptedBy = null;
1116 - markSuspendedPriorityLevel(root, expirationTime);
1119 + markSuspendedPriorityLevel(root, expirationTime, nextRenderDidError);
1120 const suspendedExpirationTime = expirationTime;
1121 const newExpirationTime = root.expirationTime;
1122 onSuspend(
@@ -1290,6 +1293,10 @@ function markTimeout(
1293 }
1294 }
1295
1296 +function markError(root: FiberRoot) {
1297 + nextRenderDidError = true;
1298 +}
1299 +
1300 function retrySuspendedRoot(root: FiberRoot, suspendedTime: ExpirationTime) {
1301 markPingedPriorityLevel(root, suspendedTime);
1302 const retryTime = root.expirationTime;
@@ -1975,6 +1982,7 @@ export {
1982 captureCommitPhaseError,
1983 onUncaughtError,
1984 markTimeout,
1985 + markError,
1986 retrySuspendedRoot,
1987 markLegacyErrorBoundaryAsFailed,
1988 isAlreadyFailedLegacyErrorBoundary,
packages/react-reconciler/src/ReactFiberUnwindWork.js
+16 -14
@@ -56,6 +56,7 @@ import {
56 } from './ReactProfilerTimer';
57 import {
58 markTimeout,
59 + markError,
60 onUncaughtError,
61 markLegacyErrorBoundaryAsFailed,
62 isAlreadyFailedLegacyErrorBoundary,
@@ -64,6 +65,7 @@ import {
65 scheduleWork,
66 retrySuspendedRoot,
67 } from './ReactFiberScheduler';
68 +import {hasLowerPriorityWork} from './ReactFiberPendingPriority';
69
70 function createRootErrorUpdate(
71 fiber: Fiber,
@@ -150,20 +152,6 @@ function throwException(
152 // Its effect list is no longer valid.
153 sourceFiber.firstEffect = sourceFiber.lastEffect = null;
154
153 - // Check if there is lower priority work, regardless of whether it's pending.
154 - // If so, it may have the effect of fixing the exception that was just thrown.
155 - const latestPendingTime = root.latestPendingTime;
156 - const latestSuspendedTime = root.latestSuspendedTime;
157 - if (
158 - renderExpirationTime !== latestPendingTime &&
159 - renderExpirationTime !== latestSuspendedTime &&
160 - renderExpirationTime !== Never
161 - ) {
162 - // There's lower priority work. Exit to suspend this render and retry at
163 - // the lower priority.
164 - return;
165 - }
166 -
155 if (
156 enableSuspense &&
157 value !== null &&
@@ -259,6 +247,20 @@ function throwException(
247 workInProgress = workInProgress.return;
248 } while (workInProgress !== null);
249 }
250 + } else {
251 + // This is an error.
252 + markError(root);
253 + if (
254 + // Retry (at the same priority) one more time before handling the error.
255 + // The retry will flush synchronously. (Unless we're already rendering
256 + // synchronously, in which case move to the next check.)
257 + (!root.didError && renderExpirationTime !== Sync) ||
258 + // There's lower priority work. If so, it may have the effect of fixing
259 + // the exception that was just thrown.
260 + hasLowerPriorityWork(root, renderExpirationTime)
261 + ) {
262 + return;
263 + }
264 }
265
266 // We didn't find a boundary that could handle this type of exception. Start
packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js
+121 -108
@@ -64,7 +64,7 @@ describe('ReactIncrementalErrorHandling', () => {
64
65 function Indirection(props) {
66 ReactNoop.yield('Indirection');
67 - return props.children;
67 + return props.children || null;
68 }
69
70 function BadRender() {
@@ -78,123 +78,45 @@ describe('ReactIncrementalErrorHandling', () => {
78 <Indirection>
79 <Indirection>
80 <BadRender />
81 + <Indirection />
82 + <Indirection />
83 </Indirection>
84 </Indirection>
85 </Indirection>
86 </ErrorBoundary>,
87 );
88
89 + // Start rendering asynchronsouly
90 ReactNoop.flushThrough([
91 'ErrorBoundary (try)',
92 'Indirection',
93 'Indirection',
94 'Indirection',
92 - // The error was thrown, but React ran out of time and yielded
93 - // before recovering.
95 + // An error is thrown. React keeps rendering asynchronously.
96 'throw',
97 ]);
98
97 - // Upon resuming, componentDidCatch is called
98 - ReactNoop.flushThrough([
99 - 'componentDidCatch',
100 - 'ErrorBoundary (catch)',
101 - 'ErrorMessage',
102 - ]);
103 - expect(ReactNoop.getChildren()).toEqual([span('Caught an error: oops!')]);
104 - });
105 -
106 - it('can recover from an error within a single render phase', () => {
107 - class Sibling extends React.Component {
108 - componentWillUnmount() {
109 - ReactNoop.yield('Unmount Sibling');
110 - }
111 - render() {
112 - return null;
113 - }
114 - }
115 -
116 - class ErrorBoundary extends React.Component {
117 - state = {error: null};
118 - static getDerivedStateFromCatch(error) {
119 - ReactNoop.yield('getDerivedStateFromCatch');
120 - return {error};
121 - }
122 - render() {
123 - if (this.state.error) {
124 - ReactNoop.yield('ErrorBoundary (catch)');
125 - } else {
126 - ReactNoop.yield('ErrorBoundary (try)');
127 - }
128 - return (
129 - <React.Fragment>
130 - <Sibling />
131 - {this.state.error ? (
132 - <ErrorMessage error={this.state.error} />
133 - ) : (
134 - this.props.children
135 - )}
136 - </React.Fragment>
137 - );
138 - }
139 - }
140 -
141 - function ErrorMessage(props) {
142 - ReactNoop.yield('ErrorMessage');
143 - return <span prop={`Caught an error: ${props.error.message}`} />;
144 - }
145 -
146 - function Indirection(props) {
147 - ReactNoop.yield('Indirection');
148 - return props.children;
149 - }
99 + // Still rendering async...
100 + ReactNoop.flushThrough(['Indirection']);
101
151 - function BadRender() {
152 - ReactNoop.yield('throw');
153 - throw new Error('oops!');
154 - }
155 -
156 - function App(props) {
157 - return (
158 - <ErrorBoundary>
159 - <Indirection>
160 - <Indirection>
161 - <Indirection>
162 - {props.shouldThrow ? <BadRender /> : null}
163 - </Indirection>
164 - </Indirection>
165 - </Indirection>
166 - </ErrorBoundary>
167 - );
168 - }
169 -
170 - ReactNoop.render(<App shouldThrow={false} />);
171 - ReactNoop.flush();
172 -
173 - ReactNoop.render(<App shouldThrow={true} />);
102 ReactNoop.flushThrough([
103 + 'Indirection',
104 + // Now that the tree is complete, and there's no remaining work, React
105 + // reverts to sync mode to retry one more time before handling the error.
106 +
107 'ErrorBoundary (try)',
108 'Indirection',
109 'Indirection',
110 'Indirection',
179 - // The error was thrown, but React ran out of time and yielded
180 - // before recovering.
181 - 'throw',
182 - ]);
183 -
184 - // Nothing committed yet.
185 - expect(ReactNoop.getChildren()).toEqual([]);
111
187 - // Upon resuming, getDerivedStateFromCatch is called
188 - ReactNoop.flushThrough([
189 - 'getDerivedStateFromCatch',
112 + // The error was thrown again. Now we can handle it.
113 + 'throw',
114 + 'Indirection',
115 + 'Indirection',
116 + 'componentDidCatch',
117 'ErrorBoundary (catch)',
118 'ErrorMessage',
119 ]);
193 - // Still hasn't committed
194 - expect(ReactNoop.getChildren()).toEqual([]);
195 - // The sibling should be re-mounted because it is the child of a failed
196 - // error boundary
197 - expect(ReactNoop.flush()).toEqual(['Unmount Sibling']);
120 expect(ReactNoop.getChildren()).toEqual([span('Caught an error: oops!')]);
121 });
122
@@ -279,6 +201,50 @@ describe('ReactIncrementalErrorHandling', () => {
201 expect(ReactNoop.getChildren()).toEqual([span('(empty)')]);
202 });
203
204 + it('retries one more time before handling error', () => {
205 + let ops = [];
206 + function BadRender() {
207 + ops.push('BadRender');
208 + ReactNoop.yield('BadRender');
209 + throw new Error('oops');
210 + }
211 +
212 + function Sibling() {
213 + ops.push('Sibling');
214 + ReactNoop.yield('Sibling');
215 + return <span prop="Sibling" />;
216 + }
217 +
218 + function Parent() {
219 + ops.push('Parent');
220 + ReactNoop.yield('Parent');
221 + return (
222 + <React.Fragment>
223 + <BadRender />
224 + <Sibling />
225 + </React.Fragment>
226 + );
227 + }
228 +
229 + ReactNoop.render(<Parent />);
230 +
231 + // Render the bad component asynchronously
232 + ReactNoop.flushThrough(['Parent', 'BadRender']);
233 +
234 + // Finish the rest of the async work
235 + ReactNoop.flushThrough(['Sibling']);
236 +
237 + // Rendering one more unit of work should be enough to trigger the retry
238 + // and synchronously throw an error.
239 + ops = [];
240 + expect(() => ReactNoop.flushUnitsOfWork(1)).toThrow('oops');
241 + expect(ops).toEqual(['Parent', 'BadRender', 'Sibling']);
242 + });
243 +
244 + // TODO: This is currently unobservable, but will be once we lift renderRoot
245 + // and commitRoot into the renderer.
246 + // it("does not retry synchronously if there's an update between complete and commit");
247 +
248 it('calls componentDidCatch multiple times for multiple errors', () => {
249 let id = 0;
250 class BadMount extends React.Component {
@@ -389,6 +355,11 @@ describe('ReactIncrementalErrorHandling', () => {
355 expect(ReactNoop.getChildren()).toEqual([]);
356
357 expect(ReactNoop.flush()).toEqual([
358 + 'BrokenRender',
359 + // React retries one more time
360 + 'ErrorBoundary render success',
361 +
362 + // Errored again on retry. Now handle it.
363 'BrokenRender',
364 'ErrorBoundary componentDidCatch',
365 'ErrorBoundary render error',
@@ -432,6 +403,7 @@ describe('ReactIncrementalErrorHandling', () => {
403 expect(ops).toEqual([
404 'ErrorBoundary render success',
405 'BrokenRender',
406 + // React doesn't retry because we're already rendering synchronously.
407 'ErrorBoundary componentDidCatch',
408 'ErrorBoundary render error',
409 ]);
@@ -475,6 +447,7 @@ describe('ReactIncrementalErrorHandling', () => {
447 expect(ops).toEqual([
448 'ErrorBoundary render success',
449 'BrokenRender',
450 + // React doesn't retry because we're already rendering synchronously.
451 'ErrorBoundary componentDidCatch',
452 'ErrorBoundary render error',
453 ]);
@@ -511,6 +484,12 @@ describe('ReactIncrementalErrorHandling', () => {
484 expect(ops).toEqual([
485 'RethrowErrorBoundary render',
486 'BrokenRender',
487 +
488 + // React retries one more time
489 + 'RethrowErrorBoundary render',
490 + 'BrokenRender',
491 +
492 + // Errored again on retry. Now handle it.
493 'RethrowErrorBoundary componentDidCatch',
494 ]);
495 expect(ReactNoop.getChildren()).toEqual([]);
@@ -549,6 +528,12 @@ describe('ReactIncrementalErrorHandling', () => {
528 }).toThrow('Hello');
529 expect(ops).toEqual([
530 'BrokenRender',
531 +
532 + // React retries one more time
533 + 'RethrowErrorBoundary render',
534 + 'BrokenRender',
535 +
536 + // Errored again on retry. Now handle it.
537 'RethrowErrorBoundary componentDidCatch',
538 ]);
539 expect(ReactNoop.getChildren()).toEqual([]);
@@ -584,6 +569,7 @@ describe('ReactIncrementalErrorHandling', () => {
569 expect(ops).toEqual([
570 'RethrowErrorBoundary render',
571 'BrokenRender',
572 + // React doesn't retry because we're already rendering synchronously.
573 'RethrowErrorBoundary componentDidCatch',
574 ]);
575 expect(ReactNoop.getChildren()).toEqual([]);
@@ -622,6 +608,7 @@ describe('ReactIncrementalErrorHandling', () => {
608 expect(ops).toEqual([
609 'RethrowErrorBoundary render',
610 'BrokenRender',
611 + // React doesn't retry because we're already rendering synchronously.
612 'RethrowErrorBoundary componentDidCatch',
613 ]);
614 expect(ReactNoop.getChildren()).toEqual([]);
@@ -688,7 +675,12 @@ describe('ReactIncrementalErrorHandling', () => {
675 expect(() => {
676 ReactNoop.flush();
677 }).toThrow('Hello');
691 - expect(ops).toEqual(['BrokenRender']);
678 + expect(ops).toEqual([
679 + 'BrokenRender',
680 + // React retries one more time
681 + 'BrokenRender',
682 + // Errored again on retry
683 + ]);
684 ops = [];
685 ReactNoop.render(<Foo />);
686 ReactNoop.flush();
@@ -719,7 +711,12 @@ describe('ReactIncrementalErrorHandling', () => {
711 ReactNoop.render(<BrokenRender throw={true} />);
712 ReactNoop.flush();
713 }).toThrow('Hello');
722 - expect(ops).toEqual(['BrokenRender']);
714 + expect(ops).toEqual([
715 + 'BrokenRender',
716 + // React retries one more time
717 + 'BrokenRender',
718 + // Errored again on retry
719 + ]);
720
721 ops = [];
722 ReactNoop.render(<Foo />);
@@ -1032,9 +1029,11 @@ describe('ReactIncrementalErrorHandling', () => {
1029 <BrokenRender />
1030 </ErrorBoundary>,
1031 );
1035 - expect(ReactNoop.flush).toWarnDev(
1032 + expect(ReactNoop.flush).toWarnDev([
1033 'Warning: React.createElement: type is invalid -- expected a string',
1037 - );
1034 + // React retries once on error
1035 + 'Warning: React.createElement: type is invalid -- expected a string',
1036 + ]);
1037 expect(ReactNoop.getChildren()).toEqual([
1038 span(
1039 'Element type is invalid: expected a string (for built-in components) or ' +
@@ -1079,9 +1078,11 @@ describe('ReactIncrementalErrorHandling', () => {
1078 <BrokenRender fail={true} />
1079 </ErrorBoundary>,
1080 );
1082 - expect(ReactNoop.flush).toWarnDev(
1081 + expect(ReactNoop.flush).toWarnDev([
1082 'Warning: React.createElement: type is invalid -- expected a string',
1084 - );
1083 + // React retries once on error
1084 + 'Warning: React.createElement: type is invalid -- expected a string',
1085 + ]);
1086 expect(ReactNoop.getChildren()).toEqual([
1087 span(
1088 'Element type is invalid: expected a string (for built-in components) or ' +
@@ -1273,6 +1274,13 @@ describe('ReactIncrementalErrorHandling', () => {
1274 'ErrorBoundary (try)',
1275 'Indirection',
1276 'BadRender',
1277 +
1278 + // React retries one more time
1279 + 'ErrorBoundary (try)',
1280 + 'Indirection',
1281 + 'BadRender',
1282 +
1283 + // Errored again on retry. Now handle it.
1284 'componentDidCatch',
1285 'ErrorBoundary (catch)',
1286 ]);
@@ -1337,7 +1345,14 @@ describe('ReactIncrementalErrorHandling', () => {
1345 // Continue rendering siblings after BadRender throws
1346 'BadRenderSibling',
1347 'BadRenderSibling',
1340 - // Recover from the error
1348 +
1349 + // React retries one more time
1350 + 'ErrorBoundary (try)',
1351 + 'throw',
1352 + 'BadRenderSibling',
1353 + 'BadRenderSibling',
1354 +
1355 + // Errored again on retry. Now handle it.
1356 'componentDidCatch',
1357 'ErrorBoundary (catch)',
1358 'ErrorMessage',
@@ -1354,7 +1369,6 @@ describe('ReactIncrementalErrorHandling', () => {
1369 throw new Error('oops!');
1370 }
1371
1357 - let parent;
1372 class Parent extends React.Component {
1373 state = {error: null, other: false};
1374 componentDidCatch(error) {
@@ -1365,7 +1379,6 @@ describe('ReactIncrementalErrorHandling', () => {
1379 ReactNoop.yield('did update');
1380 }
1381 render() {
1368 - parent = this;
1382 if (this.state.error) {
1383 ReactNoop.yield('render error message');
1384 return <span prop={`Caught an error: ${this.state.error.message}`} />;
@@ -1376,11 +1389,11 @@ describe('ReactIncrementalErrorHandling', () => {
1389 }
1390
1391 ReactNoop.render(<Parent step={1} />);
1379 - ReactNoop.flushThrough(['render', 'throw']);
1380 - expect(ReactNoop.getChildren()).toEqual([]);
1381 -
1382 - parent.setState({other: true});
1383 - expect(ReactNoop.flush()).toEqual([
1392 + ReactNoop.flushThrough([
1393 + 'render',
1394 + 'throw',
1395 + 'render',
1396 + 'throw',
1397 'did catch',
1398 'render error message',
1399 'did update',
packages/react-reconciler/src/__tests__/ReactIncrementalErrorReplay-test.js
+2 -2
@@ -36,7 +36,7 @@ describe('ReactIncrementalErrorReplay', () => {
36 });
37 });
38
39 - it('should fail gracefully on error that does not reproduce on replay', () => {
39 + it("should ignore error if it doesn't throw on retry", () => {
40 let didInit = false;
41
42 function badLazyInit() {
@@ -54,6 +54,6 @@ describe('ReactIncrementalErrorReplay', () => {
54 }
55 }
56 ReactNoop.render(<App />);
57 - expect(() => ReactNoop.flush()).toThrow('Hi');
57 + expect(() => ReactNoop.flush()).not.toThrow();
58 });
59 });
packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js
+12
@@ -14,6 +14,7 @@ describe('ReactSuspense', () => {
14 jest.resetModules();
15 ReactFeatureFlags = require('shared/ReactFeatureFlags');
16 ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
17 + ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
18 ReactFeatureFlags.enableSuspense = true;
19 React = require('react');
20 Fragment = React.Fragment;
@@ -258,6 +259,11 @@ describe('ReactSuspense', () => {
259 expect(ReactNoop.flush()).toEqual([
260 'Promise rejected [Result]',
261 'Error! [Result]',
262 +
263 + // React retries one more time
264 + 'Error! [Result]',
265 +
266 + // Errored again on retry. Now handle it.
267 'Caught error: Failed to load: Result',
268 ]);
269 expect(ReactNoop.getChildren()).toEqual([
@@ -320,6 +326,12 @@ describe('ReactSuspense', () => {
326 expect(ReactNoop.flush()).toEqual([
327 'Promise rejected [Result]',
328 'Error! [Result]',
329 +
330 + // React retries one more time
331 + 'Error! [Result]',
332 +
333 + // Errored again on retry. Now handle it.
334 +
335 'Caught error: Failed to load: Result',
336 ]);
337 expect(ReactNoop.getChildren()).toEqual([
packages/react-reconciler/src/__tests__/__snapshots__/ReactIncrementalPerf-test.internal.js.snap
+10
@@ -286,6 +286,12 @@ exports[`ReactDebugFiberPerf recovers from caught errors 1`] = `
286 "⚛ (Waiting for async callback... will force flush in 5250 ms)
287
288 // Stop on Baddie and restart from Boundary
289 +⚛ (React Tree Reconciliation: Yielded)
290 + ⚛ Parent [mount]
291 + ⚛ Boundary [mount]
292 + ⚛ Parent [mount]
293 + ⚛ Baddie [mount]
294 +
295 ⚛ (React Tree Reconciliation: Yielded)
296 ⚛ Parent [mount]
297 ⛔ Boundary [mount] Warning: An error was thrown inside this error boundary
@@ -320,6 +326,10 @@ exports[`ReactDebugFiberPerf recovers from fatal errors 1`] = `
326 ⚛ Parent [mount]
327 ⚛ Baddie [mount]
328
329 +⚛ (React Tree Reconciliation: Yielded)
330 + ⚛ Parent [mount]
331 + ⚛ Baddie [mount]
332 +
333 ⚛ (React Tree Reconciliation: Completed Root)
334
335 ⚛ (Committing Changes)
packages/react/src/__tests__/forwardRef-test.internal.js
+8
@@ -18,6 +18,7 @@ describe('forwardRef', () => {
18 jest.resetModules();
19 ReactFeatureFlags = require('shared/ReactFeatureFlags');
20 ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
21 + ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
22 React = require('react');
23 ReactNoop = require('react-noop-renderer');
24 });
@@ -175,6 +176,13 @@ describe('forwardRef', () => {
176 'ErrorBoundary.render: try',
177 'Wrapper',
178 'BadRender throw',
179 +
180 + // React retries one more time
181 + 'ErrorBoundary.render: try',
182 + 'Wrapper',
183 + 'BadRender throw',
184 +
185 + // Errored again on retry. Now handle it.
186 'ErrorBoundary.componentDidCatch',
187 'ErrorBoundary.render: catch',
188 ]);