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
]);