@samitouri / QOS-React / commits / e7eed5874f

[Fiber] New error boundary semantics (#8304)

* Exit early in scheduleUpdate if a node's priority matches This is a performance optimization and is unobservable. However, it helps protect against regressions on the following invariants on which it relies: - The priority of a fiber is greater than or equal to the priority of all its descendent fibers. - If a tree has pending work priority, its root is scheduled. * New error boundary semantics - Recovering from an error boundary no longer uses Task priority by default. The work is scheduled using whatever priority created the error. - Error handling is now a side-effect that happens during the commit phase. - The default behavior of an error boundary is to render null. Errors do not propagate except when an boundary fails. Conceptually, this would be like throwing an error from a catch block. - A host container is treated like a no-op error boundary. The first error captured by a host container is thrown at the end of the batch. Like with normal error boundaries, the entire tree is unmounted. * Fix broken setState callback test * Add test for "unwinding" context when an error interrupts rendering * Switch over primary effect types only This avoids the need to create an export for every combination of bits. * Only continue the work loop if the error was successfully captured * Add more tests for incremental error handling These tests are currently failing: ✕ catches render error in a boundary during partial deferred mounting ✕ catches render error in a boundary during animation mounting ✕ propagates an error from a noop error boundary during full deferred mounting ✕ propagates an error from a noop error boundary during partial deferred mounting ✕ propagates an error from a noop error boundary during animation mounting The observed behavior is that unstable_handleError() unexpected gets called twice: "ErrorBoundary render success", "BrokenRender", "ErrorBoundary unstable_handleError", + "ErrorBoundary render success", + "BrokenRender", + "ErrorBoundary unstable_handleError", "ErrorBoundary render error" * Verify batched updates get scheduled despite errors * Add try/catch/finally blocks around commit phase passes We'll consolidate all these blocks in a future PR that refactors the commit phase to be separate from the perform work loop. * NoopBoundary -> RethrowBoundary * Only throw uncaught error once there is no more work to perform * Remove outdated comment It was fixed in #8451. * Record tests * Always reset nextUnitOfWork on error This is important so that the test at the end of performAndHandleErrors() knows it's safe to rethrow. * Add a passing test for unmounting behavior on crashed tree * Top-level errors An error thrown from a host container should be "captured" by the host container itself * Remove outdated comment * Separate Rethrow and Noop scenarios in boundary tests * Move try block outside the commit loops

Andrew Clark committed Nov 30, 2016 at 16:54 UTC e7eed5874f0e7392789e716587ad6e071d98688a
9 files changed +1161 -482
scripts/fiber/tests-passing.txt
+17 -2
@@ -1080,16 +1080,29 @@ src/renderers/shared/fiber/__tests__/ReactIncremental-test.js
1080 * reads context when setState is above the provider
1081
1082 src/renderers/shared/fiber/__tests__/ReactIncrementalErrorHandling-test.js
1083 -* catches render error in a boundary during mounting
1084 -* propagates an error from a noop error boundary
1083 +* catches render error in a boundary during full deferred mounting
1084 +* catches render error in a boundary during partial deferred mounting
1085 +* catches render error in a boundary during animation mounting
1086 +* catches render error in a boundary during synchronous mounting
1087 +* catches render error in a boundary during batched mounting
1088 +* propagates an error from a noop error boundary during full deferred mounting
1089 +* propagates an error from a noop error boundary during partial deferred mounting
1090 +* propagates an error from a noop error boundary during animation mounting
1091 +* propagates an error from a noop error boundary during synchronous mounting
1092 +* propagates an error from a noop error boundary during batched mounting
1093 +* applies batched updates regardless despite errors in scheduling
1094 +* applies nested batched updates despite errors in scheduling
1095 +* applies sync updates regardless despite errors in scheduling
1096 * can schedule updates after uncaught error in render on mount
1097 * can schedule updates after uncaught error in render on update
1098 * can schedule updates after uncaught error during umounting
1099 * continues work on other roots despite caught errors
1100 * continues work on other roots despite uncaught errors
1101 +* unwinds the context stack correctly on error
1102 * catches reconciler errors in a boundary during mounting
1103 * catches reconciler errors in a boundary during update
1104 * recovers from uncaught reconciler errors
1105 +* unmounts components with uncaught errors
1106
1107 src/renderers/shared/fiber/__tests__/ReactIncrementalReflection-test.js
1108 * handles isMounted even when the initial render is deferred
@@ -1323,6 +1336,8 @@ src/renderers/shared/shared/__tests__/ReactErrorBoundaries-test.js
1336 * catches errors in componentDidUpdate
1337 * propagates errors inside boundary during componentDidMount
1338 * lets different boundaries catch their own first errors
1339 +* discards a bad root if the root component fails
1340 +* renders empty output if error boundary does not handle the error
1341
1342 src/renderers/shared/shared/__tests__/ReactIdentity-test.js
1343 * should allow key property to express identity
src/renderers/shared/fiber/ReactFiberBeginWork.js
+23
@@ -481,8 +481,31 @@ module.exports = function<T, P, I, TI, C>(
481 }
482 }
483
484 + function beginFailedWork(current : ?Fiber, workInProgress : Fiber, priorityLevel : PriorityLevel) {
485 + if (workInProgress.tag !== ClassComponent &&
486 + workInProgress.tag !== HostContainer) {
487 + throw new Error('Invalid type of work');
488 + }
489 +
490 + if (workInProgress.pendingWorkPriority === NoWork ||
491 + workInProgress.pendingWorkPriority > priorityLevel) {
492 + return bailoutOnLowPriority(current, workInProgress);
493 + }
494 +
495 + // If we don't bail out, we're going be recomputing our children so we need
496 + // to drop our effect list.
497 + workInProgress.firstEffect = null;
498 + workInProgress.lastEffect = null;
499 +
500 + // Unmount the current children as if the component rendered null
501 + const nextChildren = null;
502 + reconcileChildren(current, workInProgress, nextChildren);
503 + return workInProgress.child;
504 + }
505 +
506 return {
507 beginWork,
508 + beginFailedWork,
509 };
510
511 };
src/renderers/shared/fiber/ReactFiberCommitWork.js
+8 -4
@@ -34,7 +34,7 @@ var {
34
35 module.exports = function<T, P, I, TI, C>(
36 config : HostConfig<T, P, I, TI, C>,
37 - trapError : (failedFiber : Fiber, error: Error, isUnmounting : boolean) => void
37 + captureError : (failedFiber : Fiber, error: Error, isUnmounting : boolean) => Fiber | null
38 ) {
39
40 const commitUpdate = config.commitUpdate;
@@ -133,6 +133,10 @@ module.exports = function<T, P, I, TI, C>(
133 }
134
135 function commitPlacement(finishedWork : Fiber) : void {
136 + // Clear effect from effect tag before any errors can be thrown, so that
137 + // we don't attempt to do this again
138 + finishedWork.effectTag &= ~Placement;
139 +
140 // Recursively insert all host nodes into the parent.
141 const parent = getHostParent(finishedWork);
142 const before = getHostSibling(finishedWork);
@@ -272,7 +276,7 @@ module.exports = function<T, P, I, TI, C>(
276 if (typeof instance.componentWillUnmount === 'function') {
277 const error = tryCallComponentWillUnmount(instance);
278 if (error) {
275 - trapError(current, error, true);
279 + captureError(current, error, true);
280 }
281 }
282 return;
@@ -362,7 +366,7 @@ module.exports = function<T, P, I, TI, C>(
366 }
367 }
368 if (firstError) {
365 - trapError(finishedWork, firstError, false);
369 + captureError(finishedWork, firstError, false);
370 }
371 return;
372 }
@@ -375,7 +379,7 @@ module.exports = function<T, P, I, TI, C>(
379 firstError = callCallbacks(callbackList, rootFiber.current.child.stateNode);
380 }
381 if (firstError) {
378 - trapError(rootFiber, firstError, false);
382 + captureError(rootFiber, firstError, false);
383 }
384 return;
385 }
src/renderers/shared/fiber/ReactFiberContext.js
+15 -2
@@ -69,16 +69,19 @@ exports.hasContextChanged = function() : boolean {
69 function isContextProvider(fiber : Fiber) : boolean {
70 return (
71 fiber.tag === ClassComponent &&
72 + // Instance might be null, if the fiber errored during construction
73 + fiber.stateNode &&
74 typeof fiber.stateNode.getChildContext === 'function'
75 );
76 }
77 exports.isContextProvider = isContextProvider;
78
77 -exports.popContextProvider = function() : void {
79 +function popContextProvider() : void {
80 contextStack[index] = emptyObject;
81 didPerformWorkStack[index] = false;
82 index--;
81 -};
83 +}
84 +exports.popContextProvider = popContextProvider;
85
86 exports.pushTopLevelContextObject = function(context : Object, didChange : boolean) : void {
87 invariant(index === -1, 'Unexpected context found on stack');
@@ -149,3 +152,13 @@ exports.findCurrentUnmaskedContext = function(fiber: Fiber) : Object {
152 }
153 return node.stateNode.context;
154 };
155 +
156 +exports.unwindContext = function(from : Fiber, to: Fiber) {
157 + let node = from;
158 + while (node && (node !== to) && (node.alternate !== to)) {
159 + if (isContextProvider(node)) {
160 + popContextProvider();
161 + }
162 + node = node.return;
163 + }
164 +};
src/renderers/shared/fiber/ReactFiberScheduler.js
+251 -238
@@ -41,10 +41,7 @@ var {
41 PlacementAndUpdate,
42 Deletion,
43 Callback,
44 - PlacementAndCallback,
45 - UpdateAndCallback,
46 - PlacementAndUpdateAndCallback,
47 - DeletionAndCallback,
44 + Err,
45 } = require('ReactTypeOfSideEffect');
46
47 var {
@@ -52,23 +49,22 @@ var {
49 ClassComponent,
50 } = require('ReactTypeOfWork');
51
52 +var {
53 + unwindContext,
54 +} = require('ReactFiberContext');
55 +
56 if (__DEV__) {
57 var ReactFiberInstrumentation = require('ReactFiberInstrumentation');
58 }
59
60 var timeHeuristicForUnitOfWork = 1;
61
61 -type TrappedError = {
62 - boundary: Fiber | null,
63 - root: FiberRoot | null,
64 - error: any,
65 -};
66 -
62 module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
68 - const { beginWork } = ReactFiberBeginWork(config, scheduleUpdate);
63 + const { beginWork, beginFailedWork } =
64 + ReactFiberBeginWork(config, scheduleUpdate);
65 const { completeWork } = ReactFiberCompleteWork(config);
66 const { commitPlacement, commitDeletion, commitWork, commitLifeCycles } =
71 - ReactFiberCommitWork(config, trapError);
67 + ReactFiberCommitWork(config, captureError);
68
69 const hostScheduleAnimationCallback = config.scheduleAnimationCallback;
70 const hostScheduleDeferredCallback = config.scheduleDeferredCallback;
@@ -100,9 +96,8 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
96 let isAnimationCallbackScheduled : boolean = false;
97 let isDeferredCallbackScheduled : boolean = false;
98
103 - // Caught errors and error boundaries that are currently handling them
104 - let activeErrorBoundaries : Set<Fiber> | null = null;
105 - let nextTrappedErrors : Array<TrappedError> | null = null;
99 + let capturedErrors : Map<Fiber, Error> | null = null;
100 + let firstUncaughtError : Error | null = null;
101
102 function scheduleAnimationCallback(callback) {
103 if (!isAnimationCallbackScheduled) {
@@ -170,41 +165,54 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
165 // First, we'll perform all the host insertions, updates, deletions and
166 // ref unmounts.
167 let effectfulFiber = finishedWork.firstEffect;
173 - while (effectfulFiber) {
174 - switch (effectfulFiber.effectTag) {
175 - case Placement:
176 - case PlacementAndCallback: {
177 - commitPlacement(effectfulFiber);
178 - // Clear the "placement" from effect tag so that we know that this is inserted, before
179 - // any life-cycles like componentDidMount gets called.
180 - effectfulFiber.effectTag ^= Placement;
181 - break;
168 + pass1: while (true) {
169 + try {
170 + while (effectfulFiber) {
171 + // The following switch statement is only concerned about placement,
172 + // updates, and deletions. To avoid needing to add a case for every
173 + // possible bitmap value, we remove the secondary effects from the
174 + // effect tag and switch on that value.
175 + let primaryEffectTag = effectfulFiber.effectTag & ~(Callback | Err);
176 + switch (primaryEffectTag) {
177 + case Placement: {
178 + commitPlacement(effectfulFiber);
179 + // Clear the "placement" from effect tag so that we know that this is inserted, before
180 + // any life-cycles like componentDidMount gets called.
181 + effectfulFiber.effectTag &= ~Placement;
182 + break;
183 + }
184 + case PlacementAndUpdate: {
185 + // Placement
186 + commitPlacement(effectfulFiber);
187 + // Clear the "placement" from effect tag so that we know that this is inserted, before
188 + // any life-cycles like componentDidMount gets called.
189 + effectfulFiber.effectTag &= ~Placement;
190 +
191 + // Update
192 + const current = effectfulFiber.alternate;
193 + commitWork(current, effectfulFiber);
194 + break;
195 + }
196 + case Update: {
197 + const current = effectfulFiber.alternate;
198 + commitWork(current, effectfulFiber);
199 + break;
200 + }
201 + case Deletion: {
202 + commitDeletion(effectfulFiber);
203 + break;
204 + }
205 + }
206 + effectfulFiber = effectfulFiber.nextEffect;
207 }
183 - case PlacementAndUpdate:
184 - case PlacementAndUpdateAndCallback: {
185 - // Placement
186 - commitPlacement(effectfulFiber);
187 - // Clear the "placement" from effect tag so that we know that this is inserted, before
188 - // any life-cycles like componentDidMount gets called.
189 - effectfulFiber.effectTag ^= Placement;
190 -
191 - // Update
192 - const current = effectfulFiber.alternate;
193 - commitWork(current, effectfulFiber);
194 - break;
208 + } catch (error) {
209 + if (effectfulFiber) {
210 + captureError(effectfulFiber, error, false);
211 + effectfulFiber = effectfulFiber.nextEffect;
212 + continue pass1;
213 }
196 - case Update:
197 - case UpdateAndCallback:
198 - const current = effectfulFiber.alternate;
199 - commitWork(current, effectfulFiber);
200 - break;
201 - case Deletion:
202 - case DeletionAndCallback:
203 - commitDeletion(effectfulFiber);
204 - break;
214 }
206 -
207 - effectfulFiber = effectfulFiber.nextEffect;
215 + break;
216 }
217
218 // Finally if the root itself had an effect, we perform that since it is
@@ -220,35 +228,64 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
228 // happens as a separate pass so that all effects in the entire tree have
229 // already been invoked.
230 effectfulFiber = finishedWork.firstEffect;
223 - while (effectfulFiber) {
224 - if (effectfulFiber.effectTag & (Update | Callback)) {
225 - const current = effectfulFiber.alternate;
226 - const previousPriorityContext = priorityContext;
227 - priorityContext = TaskPriority;
228 - try {
229 - commitLifeCycles(current, effectfulFiber);
230 - } finally {
231 - priorityContext = previousPriorityContext;
231 + const previousPriorityContext = priorityContext;
232 + priorityContext = TaskPriority;
233 + pass2: while (true) {
234 + try {
235 + while (effectfulFiber) {
236 + const current = effectfulFiber.alternate;
237 + // Use Task priority for lifecycle updates
238 + if (effectfulFiber.effectTag & (Update | Callback)) {
239 + commitLifeCycles(current, effectfulFiber);
240 + }
241 +
242 + if (effectfulFiber.effectTag & Err) {
243 + commitErrorHandling(effectfulFiber);
244 + }
245 +
246 + const next = effectfulFiber.nextEffect;
247 + // Ensure that we clean these up so that we don't accidentally keep them.
248 + // I'm not actually sure this matters because we can't reset firstEffect
249 + // and lastEffect since they're on every node, not just the effectful
250 + // ones. So we have to clean everything as we reuse nodes anyway.
251 + effectfulFiber.nextEffect = null;
252 + // Ensure that we reset the effectTag here so that we can rely on effect
253 + // tags to reason about the current life-cycle.
254 + effectfulFiber = next;
255 + }
256 + } catch (error) {
257 + if (effectfulFiber) {
258 + captureError(effectfulFiber, error, false);
259 + const next = effectfulFiber.nextEffect;
260 + effectfulFiber.nextEffect = null;
261 + effectfulFiber = next;
262 + continue pass2;
263 }
264 }
234 - const next = effectfulFiber.nextEffect;
235 - // Ensure that we clean these up so that we don't accidentally keep them.
236 - // I'm not actually sure this matters because we can't reset firstEffect
237 - // and lastEffect since they're on every node, not just the effectful
238 - // ones. So we have to clean everything as we reuse nodes anyway.
239 - effectfulFiber.nextEffect = null;
240 - // Ensure that we reset the effectTag here so that we can rely on effect
241 - // tags to reason about the current life-cycle.
242 - effectfulFiber = next;
265 + priorityContext = previousPriorityContext;
266 + break;
267 }
268
269 // Lifecycles on the root itself
270 if (finishedWork.effectTag !== NoEffect) {
271 const current = finishedWork.alternate;
272 commitLifeCycles(current, finishedWork);
273 + if (finishedWork.effectTag & Err) {
274 + commitErrorHandling(finishedWork);
275 + }
276 + }
277 +
278 + if (capturedErrors) {
279 + if (capturedErrors.size) {
280 + capturedErrors.forEach((error, boundary) => {
281 + scheduleUpdate(boundary);
282 + });
283 + } else {
284 + capturedErrors = null;
285 + }
286 }
287
251 - // The task work includes batched updates and error handling.
288 + // Flush any task work that was scheduled during this batch
289 performTaskWork();
290 }
291
@@ -370,7 +407,15 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
407 ReactFiberInstrumentation.debugTool.onWillBeginWork(workInProgress);
408 }
409 // See if beginning this work spawns more work.
373 - let next = beginWork(current, workInProgress, nextPriorityLevel);
410 + let next;
411 + const isFailedWork = capturedErrors && capturedErrors.has(workInProgress);
412 + if (isFailedWork) {
413 + next = beginFailedWork(current, workInProgress, nextPriorityLevel);
414 + workInProgress.effectTag |= Err;
415 + } else {
416 + next = beginWork(current, workInProgress, nextPriorityLevel);
417 + }
418 +
419 if (__DEV__ && ReactFiberInstrumentation.debugTool) {
420 ReactFiberInstrumentation.debugTool.onDidBeginWork(workInProgress);
421 }
@@ -451,14 +496,7 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
496 }
497
498 function performSynchronousWork() {
454 - const prev = shouldBatchUpdates;
455 - shouldBatchUpdates = true;
456 - // All nested updates are batched
457 - try {
458 - performAndHandleErrors(SynchronousPriority);
459 - } finally {
460 - shouldBatchUpdates = prev;
461 - }
499 + performAndHandleErrors(SynchronousPriority);
500 }
501
502 function performTaskWorkUnsafe() {
@@ -491,12 +529,13 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
529 }
530
531 function performAndHandleErrors(priorityLevel : PriorityLevel, deadline : null | Deadline) {
494 - // Keep track of the first error we need to surface to the user.
495 - let firstUncaughtError = null;
496 -
532 // The exact priority level doesn't matter, so long as it's in range of the
533 // work (sync, animation, deferred) being performed.
499 - while (true) {
534 + let shouldContinue = true;
535 + while (shouldContinue) {
536 + shouldContinue = false;
537 + const prevShouldBatchUpdates = shouldBatchUpdates;
538 + shouldBatchUpdates = true;
539 try {
540 switch (priorityLevel) {
541 case SynchronousPriority:
@@ -523,171 +562,136 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
562 break;
563 }
564 } catch (error) {
526 - trapError(nextUnitOfWork, error, false);
527 - }
528 -
529 - // If there were errors and we aren't already handling them, handle them now
530 - if (nextTrappedErrors && !activeErrorBoundaries) {
531 - const nextUncaughtError = handleErrors();
532 - firstUncaughtError = firstUncaughtError || nextUncaughtError;
533 - } else {
534 - // We've done our work.
535 - break;
536 - }
565 + const failedWork = nextUnitOfWork;
566 + const boundary = captureError(failedWork, error, false);
567 + if (boundary) {
568 + // The boundary failed to complete. Complete it as if rendered null.
569 + beginFailedWork(boundary.alternate, boundary, priorityLevel);
570 +
571 + // The next unit of work is now the boundary that captured the error.
572 + // Conceptually, we're unwinding the stack. We need to unwind the
573 + // context stack, too, from the failed work to the boundary that
574 + // captured the error.
575 + // TODO: If we set the memoized props in beginWork instead of
576 + // completeWork, rather than unwind the stack, we can just restart
577 + // from the root. Can't do that until then because without memoized
578 + // props, the nodes higher up in the tree will rerender unnecessarily.
579 + if (failedWork) {
580 + unwindContext(failedWork, boundary);
581 + }
582 + nextUnitOfWork = completeUnitOfWork(boundary);
583
538 - // An error interrupted us. Now that it is handled, we may find more work.
539 - // It's safe because any roots with uncaught errors have been unscheduled.
540 - nextUnitOfWork = findNextUnitOfWork();
541 - if (!nextUnitOfWork) {
542 - // We found no other work we could do.
543 - break;
584 + // We were interupted by an error. Continue performing work.
585 + shouldContinue = true;
586 + }
587 + } finally {
588 + shouldBatchUpdates = prevShouldBatchUpdates;
589 }
590 }
591
547 - // Now it's safe to surface the first uncaught error to the user.
548 - if (firstUncaughtError) {
549 - throw firstUncaughtError;
592 + // Throw the first uncaught error
593 + if (!nextUnitOfWork && firstUncaughtError) {
594 + let e = firstUncaughtError;
595 + firstUncaughtError = null;
596 + throw e;
597 }
598 }
599
553 - function handleErrors() : Error | null {
554 - if (activeErrorBoundaries) {
555 - throw new Error('Already handling errors');
556 - }
557 -
558 - // Start tracking active boundaries.
559 - activeErrorBoundaries = new Set();
560 - // If we find unhandled errors, we'll only remember the first one.
561 - let firstUncaughtError = null;
562 -
563 - // All work created by error boundaries should have Task priority
564 - // so that it finishes before this function exits.
565 - const previousPriorityContext = priorityContext;
566 - priorityContext = TaskPriority;
600 + function captureError(failedWork : Fiber | null, error : Error, isUnmounting : boolean) : Fiber | null {
601 + // It is no longer valid because we exited the user code.
602 + ReactCurrentOwner.current = null;
603 + // It is no longer valid because this unit of work failed.
604 + nextUnitOfWork = null;
605
568 - // Keep looping until there are no more trapped errors, or until we find
569 - // an unhandled error.
570 - while (nextTrappedErrors && nextTrappedErrors.length && !firstUncaughtError) {
571 - // First, find all error boundaries and notify them about errors.
572 - while (nextTrappedErrors && nextTrappedErrors.length) {
573 - const trappedError = nextTrappedErrors.shift();
574 - const boundary = trappedError.boundary;
575 - const error = trappedError.error;
576 - const root = trappedError.root;
577 - if (!boundary) {
578 - firstUncaughtError = firstUncaughtError || error;
579 - if (root && root.current) {
580 - // Unschedule this particular root since it fataled and we can't do
581 - // more work on it. This lets us continue working on other roots
582 - // even if one of them fails before rethrowing the error.
583 - root.current.pendingWorkPriority = NoWork;
584 - } else {
585 - // Normally we should know which root caused the error, so it is
586 - // unusual if we end up here. Since we assume this function always
587 - // unschedules failed roots, our only resort is to completely
588 - // unschedule all roots. Otherwise we may get into an infinite loop
589 - // trying to resume work and finding the failing but unknown root again.
590 - nextScheduledRoot = null;
591 - lastScheduledRoot = null;
606 + // Ignore this error if it's the result of unmounting a failed boundary
607 + if (failedWork &&
608 + isUnmounting &&
609 + capturedErrors &&
610 + capturedErrors.has(failedWork)) {
611 + return null;
612 + }
613 +
614 + // Search for the nearest error boundary.
615 + let boundary : Fiber | null = null;
616 + if (failedWork) {
617 + // Host containers are a special case. If the failed work itself is a host
618 + // container, then it acts as its own boundary. In all other cases, we
619 + // ignore the work itself and only search through the parents.
620 + if (failedWork.tag === HostContainer) {
621 + boundary = failedWork;
622 + } else {
623 + let node = failedWork.return;
624 + while (node && !boundary) {
625 + if (node.tag === ClassComponent) {
626 + const instance = node.stateNode;
627 + if (typeof instance.unstable_handleError === 'function') {
628 + // Found an error boundary!
629 + boundary = node;
630 + }
631 + } else if (node.tag === HostContainer) {
632 + // Treat the root like a no-op error boundary.
633 + boundary = node;
634 }
593 - continue;
594 - }
595 - // Don't visit boundaries twice.
596 - if (activeErrorBoundaries.has(boundary)) {
597 - continue;
598 - }
599 - try {
600 - // This error boundary is now active.
601 - // We will let it handle the error and retry rendering.
602 - // If it fails again, the next error will be propagated to the parent
603 - // boundary or rethrown.
604 - activeErrorBoundaries.add(boundary);
605 - // Give error boundary a chance to update its state.
606 - // Updates will be scheduled with Task priority.
607 - const instance = boundary.stateNode;
608 - instance.unstable_handleError(error);
609 -
610 - // Schedule an update, in case the boundary didn't call setState
611 - // on itself.
612 - scheduleUpdate(boundary);
613 - } catch (nextError) {
614 - // If an error is thrown, propagate the error to the parent boundary.
615 - trapError(boundary, nextError, false);
635 + node = node.return;
636 }
637 }
638 + }
639
619 - // Now that we attempt to flush any work that was scheduled by the boundaries.
620 - // If this creates errors, they will be pushed to nextTrappedErrors and
621 - // the outer loop will continue.
622 - try {
623 - performTaskWorkUnsafe();
624 - } catch (error) {
625 - trapError(nextUnitOfWork, error, false);
640 + if (boundary) {
641 + // Add to the collection of captured errors. This is stored as a global
642 + // map of errors keyed by the boundaries that capture them. We mostly
643 + // use this Map as a Set; it's a Map only to avoid adding a field to Fiber
644 + // to store the error.
645 + if (!capturedErrors) {
646 + capturedErrors = new Map();
647 }
648 + // Ensure that neither this boundary nor its alternate has captured an
649 + // error already.
650 + if (!capturedErrors.has(boundary) &&
651 + !(boundary.alternate && capturedErrors.has(boundary.alternate))) {
652 + capturedErrors.set(boundary, error);
653 + }
654 + return boundary;
655 + } else if (!firstUncaughtError) {
656 + // If no boundary is found, we'll need to throw the error
657 + firstUncaughtError = error;
658 }
628 -
629 - nextTrappedErrors = null;
630 - activeErrorBoundaries = null;
631 - priorityContext = previousPriorityContext;
632 -
633 - // Return the error so we can rethrow after handling other roots.
634 - return firstUncaughtError;
659 + return null;
660 }
661
637 - function trapError(failedFiber : Fiber | null, error : any, isUnmounting : boolean) : void {
638 - // Don't try to start here again on next flush.
639 - nextUnitOfWork = null;
640 -
641 - // It is no longer valid because we exited the user code.
642 - ReactCurrentOwner.current = null;
643 -
644 - if (isUnmounting && activeErrorBoundaries) {
645 - // Ignore errors caused by unmounting during error handling.
646 - // This lets error boundaries safely tear down already failed trees.
647 - return;
648 - }
649 -
650 - let boundary = null;
651 - let root = null;
652 -
653 - // Search for the parent error boundary and root.
654 - let fiber = failedFiber;
655 - while (fiber) {
656 - const parent = fiber.return;
657 - if (parent) {
658 - if (parent.tag === ClassComponent && boundary === null) {
659 - // Consider a candidate for parent boundary.
660 - const instance = parent.stateNode;
661 - const isBoundary = typeof instance.unstable_handleError === 'function';
662 - if (isBoundary) {
663 - // Skip boundaries that are already active so errors can propagate.
664 - const isBoundaryAlreadyHandlingAnotherError = (
665 - activeErrorBoundaries !== null &&
666 - activeErrorBoundaries.has(parent)
667 - );
668 - if (!isBoundaryAlreadyHandlingAnotherError) {
669 - // We found the boundary.
670 - boundary = parent;
671 - }
672 - }
673 - }
674 - } else if (fiber.tag === HostContainer) {
675 - // We found the root.
676 - root = (fiber.stateNode : FiberRoot);
677 - } else {
678 - throw new Error('Invalid root');
679 - }
680 - fiber = parent;
662 + function commitErrorHandling(effectfulFiber : Fiber) {
663 + let error;
664 + if (capturedErrors) {
665 + // Get the error associated with the fiber being commited.
666 + error = capturedErrors.get(effectfulFiber);
667 + capturedErrors.delete(effectfulFiber);
668 + }
669 + if (!error) {
670 + throw new Error('No matching captured error.');
671 }
672
683 - if (!nextTrappedErrors) {
684 - nextTrappedErrors = [];
673 + switch (effectfulFiber.tag) {
674 + case ClassComponent:
675 + const instance = effectfulFiber.stateNode;
676 + try {
677 + // Allow the boundary to handle the error, usually by scheduling
678 + // an update to itself
679 + instance.unstable_handleError(error);
680 + } catch (e) {
681 + captureError(effectfulFiber, e, false);
682 + }
683 + return;
684 + case HostContainer:
685 + if (!firstUncaughtError) {
686 + // If this is the host container, we treat it as a no-op error
687 + // boundary. We'll throw the first uncaught error once it's safe to
688 + // do so, at the end of the batch.
689 + firstUncaughtError = error;
690 + }
691 + return;
692 + default:
693 + throw new Error('Invalid type of work.');
694 }
686 - nextTrappedErrors.push({
687 - boundary,
688 - error,
689 - root,
690 - });
695 }
696
697 function scheduleWork(root : FiberRoot) {
@@ -758,28 +762,37 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
762 priorityLevel = TaskPriority;
763 }
764
761 - while (true) {
762 - if (fiber.pendingWorkPriority === NoWork ||
763 - fiber.pendingWorkPriority >= priorityLevel) {
764 - fiber.pendingWorkPriority = priorityLevel;
765 + let node = fiber;
766 + let shouldContinue = true;
767 + while (node && shouldContinue) {
768 + // Walk the parent path to the root and update each node's priority. Once
769 + // we reach a node whose priority matches (and whose alternate's priority
770 + // matches) we can exit safely knowing that the rest of the path is correct.
771 + shouldContinue = false;
772 + if (node.pendingWorkPriority === NoWork ||
773 + node.pendingWorkPriority >= priorityLevel) {
774 + // Priority did not match. Update and keep going.
775 + shouldContinue = true;
776 + node.pendingWorkPriority = priorityLevel;
777 }
766 - if (fiber.alternate) {
767 - if (fiber.alternate.pendingWorkPriority === NoWork ||
768 - fiber.alternate.pendingWorkPriority >= priorityLevel) {
769 - fiber.alternate.pendingWorkPriority = priorityLevel;
778 + if (node.alternate) {
779 + if (node.alternate.pendingWorkPriority === NoWork ||
780 + node.alternate.pendingWorkPriority >= priorityLevel) {
781 + // Priority did not match. Update and keep going.
782 + shouldContinue = true;
783 + node.alternate.pendingWorkPriority = priorityLevel;
784 }
785 }
772 - if (!fiber.return) {
773 - if (fiber.tag === HostContainer) {
774 - const root : FiberRoot = (fiber.stateNode : any);
786 + if (!node.return) {
787 + if (node.tag === HostContainer) {
788 + const root : FiberRoot = (node.stateNode : any);
789 scheduleWorkAtPriority(root, priorityLevel);
776 - return;
790 } else {
791 // TODO: Warn about setting state on an unmounted component.
792 return;
793 }
794 }
782 - fiber = fiber.return;
795 + node = node.return;
796 }
797 }
798
src/renderers/shared/fiber/ReactTypeOfSideEffect.js
+8 -11
@@ -12,17 +12,14 @@
12
13 'use strict';
14
15 -export type TypeOfSideEffect = 0 | 1 | 2 | 3 | 4 | 8 | 9 | 10 | 11 | 12;
15 +export type TypeOfSideEffect = 0 | 1 | 2 | 3 | 4 | 8 | 16;
16
17 module.exports = {
18 - NoEffect: 0, // 0b0000
19 - Placement: 1, // 0b0001
20 - Update: 2, // 0b0010
21 - PlacementAndUpdate: 3, // 0b0011
22 - Deletion: 4, // 0b0100
23 - Callback: 8, // 0b1000
24 - PlacementAndCallback: 9, // 0b1001
25 - UpdateAndCallback: 10, // 0b1010
26 - PlacementAndUpdateAndCallback: 11, // 0b1011
27 - DeletionAndCallback: 12, // 0b1100
18 + NoEffect: 0, // 0b00000
19 + Placement: 1, // 0b00001
20 + Update: 2, // 0b00010
21 + PlacementAndUpdate: 3, // 0b00011
22 + Deletion: 4, // 0b00100
23 + Callback: 8, // 0b01000
24 + Err: 16, // 0b10000
25 };
src/renderers/shared/fiber/__tests__/ReactIncremental-test.js
+14 -19
@@ -1465,7 +1465,6 @@ describe('ReactIncremental', () => {
1465 ]);
1466 expect(instance.state.n).toEqual(4);
1467 });
1468 -
1468 it('can handle if setState callback throws', () => {
1469 var ops = [];
1470 var instance;
@@ -1482,30 +1481,26 @@ describe('ReactIncremental', () => {
1481 ReactNoop.flush();
1482 ops = [];
1483
1485 - expect(instance.state.n).toEqual(0);
1486 -
1487 - // first good callback
1488 - instance.setState({ n: 1 }, () => ops.push('first good callback'));
1489 - ReactNoop.flush();
1484 + function updater({ n }) {
1485 + return { n: n + 1 };
1486 + }
1487
1491 - // callback throws
1492 - instance.setState({ n: 2 }, () => {
1493 - throw new Error('Bail');
1488 + instance.setState(updater, () => ops.push('first callback'));
1489 + instance.setState(updater, () => {
1490 + ops.push('second callback');
1491 + throw new Error('callback error');
1492 });
1493 + instance.setState(updater, () => ops.push('third callback'));
1494 +
1495 expect(() => {
1496 ReactNoop.flush();
1497 - }).toThrow('Bail');
1498 -
1499 - // should set state to 2 even if callback throws up
1500 - expect(instance.state.n).toEqual(2);
1501 -
1502 - // another good callback
1503 - instance.setState({ n: 3 }, () => ops.push('second good callback'));
1504 - ReactNoop.flush();
1497 + }).toThrow('callback error');
1498
1499 + // Should call all callbacks, even though the second one throws
1500 expect(ops).toEqual([
1507 - 'first good callback',
1508 - 'second good callback',
1501 + 'first callback',
1502 + 'second callback',
1503 + 'third callback',
1504 ]);
1505 expect(instance.state.n).toEqual(3);
1506 });
src/renderers/shared/fiber/__tests__/ReactIncrementalErrorHandling-test.js
+515 -19
@@ -25,7 +25,7 @@ describe('ReactIncrementalErrorHandling', () => {
25 return { type: 'span', children: [], prop };
26 }
27
28 - it('catches render error in a boundary during mounting', () => {
28 + it('catches render error in a boundary during full deferred mounting', () => {
29 class ErrorBoundary extends React.Component {
30 state = {error: null};
31 unstable_handleError(error) {
@@ -48,31 +48,418 @@ describe('ReactIncrementalErrorHandling', () => {
48 <BrokenRender />
49 </ErrorBoundary>
50 );
51 - ReactNoop.flush();
51 + ReactNoop.flushDeferredPri();
52 expect(ReactNoop.getChildren()).toEqual([span('Caught an error: Hello.')]);
53 });
54
55 - it('propagates an error from a noop error boundary', () => {
56 - class NoopBoundary extends React.Component {
57 - unstable_handleError() {
58 - // Noop
55 + it('catches render error in a boundary during partial deferred mounting', () => {
56 + var ops = [];
57 + class ErrorBoundary extends React.Component {
58 + state = {error: null};
59 + unstable_handleError(error) {
60 + ops.push('ErrorBoundary unstable_handleError');
61 + this.setState({error});
62 }
63 render() {
64 + if (this.state.error) {
65 + ops.push('ErrorBoundary render error');
66 + return <span prop={`Caught an error: ${this.state.error.message}.`} />;
67 + }
68 + ops.push('ErrorBoundary render success');
69 return this.props.children;
70 }
71 }
72
65 - function RenderError() {
66 - throw new Error('render error');
73 + function BrokenRender(props) {
74 + ops.push('BrokenRender');
75 + throw new Error('Hello');
76 }
77
78 ReactNoop.render(
70 - <NoopBoundary>
71 - <RenderError />
72 - </NoopBoundary>
79 + <ErrorBoundary>
80 + <BrokenRender />
81 + </ErrorBoundary>
82 + );
83 +
84 + ReactNoop.flushDeferredPri(15);
85 + expect(ops).toEqual([
86 + 'ErrorBoundary render success',
87 + ]);
88 + expect(ReactNoop.getChildren()).toEqual([]);
89 +
90 + ops.length = 0;
91 + ReactNoop.flushDeferredPri(30);
92 + expect(ops).toEqual([
93 + 'BrokenRender',
94 + 'ErrorBoundary unstable_handleError',
95 + 'ErrorBoundary render error',
96 + ]);
97 + expect(ReactNoop.getChildren()).toEqual([span('Caught an error: Hello.')]);
98 + });
99 +
100 + it('catches render error in a boundary during animation mounting', () => {
101 + var ops = [];
102 + class ErrorBoundary extends React.Component {
103 + state = {error: null};
104 + unstable_handleError(error) {
105 + ops.push('ErrorBoundary unstable_handleError');
106 + this.setState({error});
107 + }
108 + render() {
109 + if (this.state.error) {
110 + ops.push('ErrorBoundary render error');
111 + return <span prop={`Caught an error: ${this.state.error.message}.`} />;
112 + }
113 + ops.push('ErrorBoundary render success');
114 + return this.props.children;
115 + }
116 + }
117 +
118 + function BrokenRender(props) {
119 + ops.push('BrokenRender');
120 + throw new Error('Hello');
121 + }
122 +
123 + ReactNoop.performAnimationWork(() => {
124 + ReactNoop.render(
125 + <ErrorBoundary>
126 + <BrokenRender />
127 + </ErrorBoundary>
128 + );
129 + });
130 +
131 + ReactNoop.flushAnimationPri();
132 + expect(ops).toEqual([
133 + 'ErrorBoundary render success',
134 + 'BrokenRender',
135 + ]);
136 + ops = [];
137 + ReactNoop.flushDeferredPri(25);
138 + expect(ops).toEqual([
139 + 'ErrorBoundary unstable_handleError',
140 + 'ErrorBoundary render error',
141 + ]);
142 + expect(ReactNoop.getChildren()).toEqual([span('Caught an error: Hello.')]);
143 + });
144 +
145 + it('catches render error in a boundary during synchronous mounting', () => {
146 + var ops = [];
147 + class ErrorBoundary extends React.Component {
148 + state = {error: null};
149 + unstable_handleError(error) {
150 + ops.push('ErrorBoundary unstable_handleError');
151 + this.setState({error});
152 + }
153 + render() {
154 + if (this.state.error) {
155 + ops.push('ErrorBoundary render error');
156 + return <span prop={`Caught an error: ${this.state.error.message}.`} />;
157 + }
158 + ops.push('ErrorBoundary render success');
159 + return this.props.children;
160 + }
161 + }
162 +
163 + function BrokenRender(props) {
164 + ops.push('BrokenRender');
165 + throw new Error('Hello');
166 + }
167 +
168 + ReactNoop.syncUpdates(() => {
169 + ReactNoop.render(
170 + <ErrorBoundary>
171 + <BrokenRender />
172 + </ErrorBoundary>
173 + );
174 + });
175 +
176 + expect(ops).toEqual([
177 + 'ErrorBoundary render success',
178 + 'BrokenRender',
179 + 'ErrorBoundary unstable_handleError',
180 + 'ErrorBoundary render error',
181 + ]);
182 + expect(ReactNoop.getChildren()).toEqual([span('Caught an error: Hello.')]);
183 + });
184 +
185 + it('catches render error in a boundary during batched mounting', () => {
186 + var ops = [];
187 + class ErrorBoundary extends React.Component {
188 + state = {error: null};
189 + unstable_handleError(error) {
190 + ops.push('ErrorBoundary unstable_handleError');
191 + this.setState({error});
192 + }
193 + render() {
194 + if (this.state.error) {
195 + ops.push('ErrorBoundary render error');
196 + return <span prop={`Caught an error: ${this.state.error.message}.`} />;
197 + }
198 + ops.push('ErrorBoundary render success');
199 + return this.props.children;
200 + }
201 + }
202 +
203 + function BrokenRender(props) {
204 + ops.push('BrokenRender');
205 + throw new Error('Hello');
206 + }
207 +
208 + ReactNoop.syncUpdates(() => {
209 + ReactNoop.batchedUpdates(() => {
210 + ReactNoop.render(
211 + <ErrorBoundary>
212 + Before the storm.
213 + </ErrorBoundary>
214 + );
215 + ReactNoop.render(
216 + <ErrorBoundary>
217 + <BrokenRender />
218 + </ErrorBoundary>
219 + );
220 + });
221 + });
222 +
223 + expect(ops).toEqual([
224 + 'ErrorBoundary render success',
225 + 'BrokenRender',
226 + 'ErrorBoundary unstable_handleError',
227 + 'ErrorBoundary render error',
228 + ]);
229 + expect(ReactNoop.getChildren()).toEqual([span('Caught an error: Hello.')]);
230 + });
231 +
232 + it('propagates an error from a noop error boundary during full deferred mounting', () => {
233 + var ops = [];
234 + class RethrowErrorBoundary extends React.Component {
235 + unstable_handleError(error) {
236 + ops.push('RethrowErrorBoundary unstable_handleError');
237 + throw error;
238 + }
239 + render() {
240 + ops.push('RethrowErrorBoundary render');
241 + return this.props.children;
242 + }
243 + }
244 +
245 + function BrokenRender() {
246 + ops.push('BrokenRender');
247 + throw new Error('Hello');
248 + }
249 +
250 + ReactNoop.render(
251 + <RethrowErrorBoundary>
252 + <BrokenRender />
253 + </RethrowErrorBoundary>
254 + );
255 +
256 + expect(() => {
257 + ReactNoop.flush();
258 + }).toThrow('Hello');
259 + expect(ops).toEqual([
260 + 'RethrowErrorBoundary render',
261 + 'BrokenRender',
262 + 'RethrowErrorBoundary unstable_handleError',
263 + ]);
264 + expect(ReactNoop.getChildren()).toEqual([]);
265 + });
266 +
267 + it('propagates an error from a noop error boundary during partial deferred mounting', () => {
268 + var ops = [];
269 + class RethrowErrorBoundary extends React.Component {
270 + unstable_handleError(error) {
271 + ops.push('RethrowErrorBoundary unstable_handleError');
272 + throw error;
273 + }
274 + render() {
275 + ops.push('RethrowErrorBoundary render');
276 + return this.props.children;
277 + }
278 + }
279 +
280 + function BrokenRender() {
281 + ops.push('BrokenRender');
282 + throw new Error('Hello');
283 + }
284 +
285 + ReactNoop.render(
286 + <RethrowErrorBoundary>
287 + <BrokenRender />
288 + </RethrowErrorBoundary>
289 );
290
75 - expect(ReactNoop.flush).toThrow('render error');
291 + ReactNoop.flushDeferredPri(15);
292 + expect(ops).toEqual([
293 + 'RethrowErrorBoundary render',
294 + ]);
295 +
296 + ops.length = 0;
297 + expect(() => {
298 + ReactNoop.flush();
299 + }).toThrow('Hello');
300 + expect(ops).toEqual([
301 + 'BrokenRender',
302 + 'RethrowErrorBoundary unstable_handleError',
303 + ]);
304 + expect(ReactNoop.getChildren()).toEqual([]);
305 + });
306 +
307 + it('propagates an error from a noop error boundary during animation mounting', () => {
308 + var ops = [];
309 + class RethrowErrorBoundary extends React.Component {
310 + unstable_handleError(error) {
311 + ops.push('RethrowErrorBoundary unstable_handleError');
312 + throw error;
313 + }
314 + render() {
315 + ops.push('RethrowErrorBoundary render');
316 + return this.props.children;
317 + }
318 + }
319 +
320 + function BrokenRender() {
321 + ops.push('BrokenRender');
322 + throw new Error('Hello');
323 + }
324 +
325 + ReactNoop.performAnimationWork(() => {
326 + ReactNoop.render(
327 + <RethrowErrorBoundary>
328 + <BrokenRender />
329 + </RethrowErrorBoundary>
330 + );
331 + });
332 +
333 + expect(() => {
334 + ReactNoop.flush();
335 + }).toThrow('Hello');
336 + expect(ops).toEqual([
337 + 'RethrowErrorBoundary render',
338 + 'BrokenRender',
339 + 'RethrowErrorBoundary unstable_handleError',
340 + ]);
341 + expect(ReactNoop.getChildren()).toEqual([]);
342 + });
343 +
344 + it('propagates an error from a noop error boundary during synchronous mounting', () => {
345 + var ops = [];
346 + class RethrowErrorBoundary extends React.Component {
347 + unstable_handleError(error) {
348 + ops.push('RethrowErrorBoundary unstable_handleError');
349 + throw error;
350 + }
351 + render() {
352 + ops.push('RethrowErrorBoundary render');
353 + return this.props.children;
354 + }
355 + }
356 +
357 + function BrokenRender() {
358 + ops.push('BrokenRender');
359 + throw new Error('Hello');
360 + }
361 +
362 + expect(() => {
363 + ReactNoop.syncUpdates(() => {
364 + ReactNoop.render(
365 + <RethrowErrorBoundary>
366 + <BrokenRender />
367 + </RethrowErrorBoundary>
368 + );
369 + });
370 + }).toThrow('Hello');
371 + expect(ops).toEqual([
372 + 'RethrowErrorBoundary render',
373 + 'BrokenRender',
374 + 'RethrowErrorBoundary unstable_handleError',
375 + ]);
376 + expect(ReactNoop.getChildren()).toEqual([]);
377 + });
378 +
379 + it('propagates an error from a noop error boundary during batched mounting', () => {
380 + var ops = [];
381 + class RethrowErrorBoundary extends React.Component {
382 + unstable_handleError(error) {
383 + ops.push('RethrowErrorBoundary unstable_handleError');
384 + throw error;
385 + }
386 + render() {
387 + ops.push('RethrowErrorBoundary render');
388 + return this.props.children;
389 + }
390 + }
391 +
392 + function BrokenRender() {
393 + ops.push('BrokenRender');
394 + throw new Error('Hello');
395 + }
396 +
397 + expect(() => {
398 + ReactNoop.syncUpdates(() => {
399 + ReactNoop.batchedUpdates(() => {
400 + ReactNoop.render(
401 + <RethrowErrorBoundary>
402 + Before the storm.
403 + </RethrowErrorBoundary>
404 + );
405 + ReactNoop.render(
406 + <RethrowErrorBoundary>
407 + <BrokenRender />
408 + </RethrowErrorBoundary>
409 + );
410 + });
411 + });
412 + }).toThrow('Hello');
413 + expect(ops).toEqual([
414 + 'RethrowErrorBoundary render',
415 + 'BrokenRender',
416 + 'RethrowErrorBoundary unstable_handleError',
417 + ]);
418 + expect(ReactNoop.getChildren()).toEqual([]);
419 + });
420 +
421 + it('applies batched updates regardless despite errors in scheduling', () => {
422 + ReactNoop.render(<span prop="a:1" />);
423 + expect(() => {
424 + ReactNoop.batchedUpdates(() => {
425 + ReactNoop.render(<span prop="a:2" />);
426 + ReactNoop.render(<span prop="a:3" />);
427 + throw new Error('Hello');
428 + });
429 + }).toThrow('Hello');
430 + ReactNoop.flush();
431 + expect(ReactNoop.getChildren()).toEqual([span('a:3')]);
432 + });
433 +
434 + it('applies nested batched updates despite errors in scheduling', () => {
435 + ReactNoop.render(<span prop="a:1" />);
436 + expect(() => {
437 + ReactNoop.batchedUpdates(() => {
438 + ReactNoop.render(<span prop="a:2" />);
439 + ReactNoop.render(<span prop="a:3" />);
440 + ReactNoop.batchedUpdates(() => {
441 + ReactNoop.render(<span prop="a:4" />);
442 + ReactNoop.render(<span prop="a:5" />);
443 + throw new Error('Hello');
444 + });
445 + });
446 + }).toThrow('Hello');
447 + ReactNoop.flush();
448 + expect(ReactNoop.getChildren()).toEqual([span('a:5')]);
449 + });
450 +
451 + it('applies sync updates regardless despite errors in scheduling', () => {
452 + ReactNoop.render(<span prop="a:1" />);
453 + expect(() => {
454 + ReactNoop.syncUpdates(() => {
455 + ReactNoop.batchedUpdates(() => {
456 + ReactNoop.render(<span prop="a:2" />);
457 + ReactNoop.render(<span prop="a:3" />);
458 + throw new Error('Hello');
459 + });
460 + });
461 + }).toThrow('Hello');
462 + expect(ReactNoop.getChildren()).toEqual([span('a:3')]);
463 });
464
465 it('can schedule updates after uncaught error in render on mount', () => {
@@ -218,9 +605,7 @@ describe('ReactIncrementalErrorHandling', () => {
605 ReactNoop.flush();
606 }).toThrow('Hello');
607 expect(ReactNoop.getChildren('a')).toEqual([span('a:3')]);
221 - // Currently we assume previous tree stays intact for fataled trees.
222 - // We may consider tearing it down in the future.
223 - expect(ReactNoop.getChildren('b')).toEqual([span('b:2')]);
608 + expect(ReactNoop.getChildren('b')).toEqual([]);
609
610 ReactNoop.renderToRootWithID(<span prop="a:4" />, 'a');
611 ReactNoop.renderToRootWithID(<BrokenRender />, 'b');
@@ -229,7 +614,7 @@ describe('ReactIncrementalErrorHandling', () => {
614 ReactNoop.flush();
615 }).toThrow('Hello');
616 expect(ReactNoop.getChildren('a')).toEqual([span('a:4')]);
232 - expect(ReactNoop.getChildren('b')).toEqual([span('b:2')]);
617 + expect(ReactNoop.getChildren('b')).toEqual([]);
618 expect(ReactNoop.getChildren('c')).toEqual([span('c:4')]);
619
620 ReactNoop.renderToRootWithID(<span prop="a:5" />, 'a');
@@ -255,9 +640,9 @@ describe('ReactIncrementalErrorHandling', () => {
640 expect(() => {
641 ReactNoop.flush();
642 }).toThrow('Hello');
258 - expect(ReactNoop.getChildren('a')).toEqual([span('a:5')]);
643 + expect(ReactNoop.getChildren('a')).toEqual([]);
644 expect(ReactNoop.getChildren('b')).toEqual([span('b:6')]);
260 - expect(ReactNoop.getChildren('c')).toEqual([span('c:5')]);
645 + expect(ReactNoop.getChildren('c')).toEqual([]);
646 expect(ReactNoop.getChildren('d')).toEqual([span('d:6')]);
647 expect(ReactNoop.getChildren('e')).toEqual([]);
648 expect(ReactNoop.getChildren('f')).toEqual([span('f:6')]);
@@ -276,6 +661,66 @@ describe('ReactIncrementalErrorHandling', () => {
661 expect(ReactNoop.getChildren('f')).toEqual(null);
662 });
663
664 + it('unwinds the context stack correctly on error', () => {
665 + class Provider extends React.Component {
666 + static childContextTypes = { message: React.PropTypes.string };
667 + static contextTypes = { message: React.PropTypes.string };
668 + getChildContext() {
669 + return {
670 + message: (this.context.message || '') + this.props.message,
671 + };
672 + }
673 + render() {
674 + return this.props.children;
675 + }
676 + }
677 +
678 + function Connector(props, context) {
679 + return <span prop={context.message} />;
680 + }
681 +
682 + Connector.contextTypes = {
683 + message: React.PropTypes.string,
684 + };
685 +
686 + function BadRender() {
687 + throw new Error('render error');
688 + }
689 +
690 + class Boundary extends React.Component {
691 + state = { error: null };
692 + unstable_handleError(error) {
693 + this.setState({ error });
694 + }
695 + render() {
696 + return (
697 + <Provider message="b">
698 + <Provider message="c">
699 + <Provider message="d">
700 + <Provider message="e">
701 + {!this.state.error && <BadRender />}
702 + </Provider>
703 + </Provider>
704 + </Provider>
705 + </Provider>
706 + );
707 + }
708 + }
709 +
710 + ReactNoop.render(
711 + <Provider message="a">
712 + <Boundary />
713 + <Connector />
714 + </Provider>
715 + );
716 + ReactNoop.flush();
717 +
718 + // If the context stack does not unwind, span will get 'abcde'
719 + expect(ReactNoop.getChildren()).toEqual([
720 + span('a'),
721 + ]);
722 + });
723 +
724 it('catches reconciler errors in a boundary during mounting', () => {
725 spyOn(console, 'error');
726
@@ -291,7 +736,6 @@ describe('ReactIncrementalErrorHandling', () => {
736 return this.props.children;
737 }
738 }
294 -
739 const InvalidType = undefined;
740 const brokenElement = <InvalidType />;
741 function BrokenRender(props) {
@@ -368,4 +812,56 @@ describe('ReactIncrementalErrorHandling', () => {
812 ReactNoop.flush();
813 expect(ReactNoop.getChildren()).toEqual([span('hi')]);
814 });
815 +
816 + it('unmounts components with uncaught errors', () => {
817 + const ops = [];
818 + let inst;
819 +
820 + class BrokenRenderAndUnmount extends React.Component {
821 + state = {fail: false};
822 + componentWillUnmount() {
823 + ops.push('BrokenRenderAndUnmount componentWillUnmount');
824 + }
825 + render() {
826 + inst = this;
827 + if (this.state.fail) {
828 + throw new Error('Hello.');
829 + }
830 + return null;
831 + }
832 + }
833 +
834 + class Parent extends React.Component {
835 + componentWillUnmount() {
836 + ops.push('Parent componentWillUnmount [!]');
837 + throw new Error('One does not simply unmount me.');
838 + }
839 + render() {
840 + return this.props.children;
841 + }
842 + }
843 +
844 + ReactNoop.render(
845 + <Parent>
846 + <Parent>
847 + <BrokenRenderAndUnmount />
848 + </Parent>
849 + </Parent>
850 + );
851 + ReactNoop.flush();
852 +
853 + inst.setState({fail: true});
854 + expect(() => {
855 + ReactNoop.flush();
856 + }).toThrowError('Hello.');
857 +
858 + expect(ops).toEqual([
859 + // Attempt to clean up.
860 + // Errors in parents shouldn't stop children from unmounting.
861 + 'Parent componentWillUnmount [!]',
862 + 'Parent componentWillUnmount [!]',
863 + 'BrokenRenderAndUnmount componentWillUnmount',
864 + ]);
865 + expect(ReactNoop.getChildren()).toEqual([]);
866 + });
867 });
src/renderers/shared/shared/__tests__/ReactErrorBoundaries-test.js
+310 -187
@@ -33,6 +33,7 @@ describe('ReactErrorBoundaries', () => {
33 var ErrorBoundary;
34 var ErrorMessage;
35 var NoopErrorBoundary;
36 + var RethrowErrorBoundary;
37 var Normal;
38
39 beforeEach(() => {
@@ -394,7 +395,12 @@ describe('ReactErrorBoundaries', () => {
395 log.push('NoopErrorBoundary componentWillUnmount');
396 }
397 unstable_handleError() {
397 - log.push('NoopErrorBoundary unstable_handleError');
398 + if (ReactDOMFeatureFlags.useFiber) {
399 + log.push('NoopErrorBoundary unstable_handleError');
400 + } else {
401 + // In Stack, not calling setState() is treated as a rethrow.
402 + log.push('NoopErrorBoundary unstable_handleError [*]');
403 + }
404 }
405 };
406
@@ -478,6 +484,35 @@ describe('ReactErrorBoundaries', () => {
484 },
485 };
486
487 + RethrowErrorBoundary = class extends React.Component {
488 + constructor(props) {
489 + super(props);
490 + log.push('RethrowErrorBoundary constructor');
491 + }
492 + render() {
493 + log.push('RethrowErrorBoundary render');
494 + return <BrokenRender />;
495 + }
496 + componentWillMount() {
497 + log.push('RethrowErrorBoundary componentWillMount');
498 + }
499 + componentDidMount() {
500 + log.push('RethrowErrorBoundary componentDidMount');
501 + }
502 + componentWillUnmount() {
503 + log.push('RethrowErrorBoundary componentWillUnmount');
504 + }
505 + unstable_handleError(error) {
506 + if (!ReactDOMFeatureFlags.useFiber) {
507 + log.push('RethrowErrorBoundary unstable_handleError [*]');
508 + // In Stack, not calling setState() is treated as a rethrow.
509 + return;
510 + }
511 + log.push('RethrowErrorBoundary unstable_handleError [!]');
512 + throw error;
513 + }
514 + };
515 +
516 ErrorMessage = class extends React.Component {
517 constructor(props) {
518 super(props);
@@ -601,16 +636,20 @@ describe('ReactErrorBoundaries', () => {
636 'BrokenRender constructor',
637 'BrokenRender componentWillMount',
638 'BrokenRender render [!]',
604 - // Catch and render an error message
605 - 'ErrorBoundary unstable_handleError',
639 ...(ReactDOMFeatureFlags.useFiber ? [
607 - // The initial render was aborted, so
608 - // Fiber retries from the root.
609 - 'ErrorBoundary constructor',
610 - 'ErrorBoundary componentWillMount',
611 - ] : []),
612 - 'ErrorBoundary render error',
613 - 'ErrorBoundary componentDidMount',
640 + // Fiber mounts with null children before capturing error
641 + 'ErrorBoundary componentDidMount',
642 + // Catch and render an error message
643 + 'ErrorBoundary unstable_handleError',
644 + 'ErrorBoundary componentWillUpdate',
645 + 'ErrorBoundary render error',
646 + 'ErrorBoundary componentDidUpdate',
647 + ] : [
648 + // Catch and render an error message
649 + 'ErrorBoundary unstable_handleError',
650 + 'ErrorBoundary render error',
651 + 'ErrorBoundary componentDidMount',
652 + ]),
653 ]);
654
655 log.length = 0;
@@ -634,16 +673,20 @@ describe('ReactErrorBoundaries', () => {
673 'ErrorBoundary componentWillMount',
674 'ErrorBoundary render success',
675 'BrokenConstructor constructor [!]',
637 - // Catch and render an error message
638 - 'ErrorBoundary unstable_handleError',
676 ...(ReactDOMFeatureFlags.useFiber ? [
640 - // The initial render was aborted, so
641 - // Fiber retries from the root.
642 - 'ErrorBoundary constructor',
643 - 'ErrorBoundary componentWillMount',
644 - ] : []),
645 - 'ErrorBoundary render error',
646 - 'ErrorBoundary componentDidMount',
677 + // Fiber mounts with null children before capturing error
678 + 'ErrorBoundary componentDidMount',
679 + // Catch and render an error message
680 + 'ErrorBoundary unstable_handleError',
681 + 'ErrorBoundary componentWillUpdate',
682 + 'ErrorBoundary render error',
683 + 'ErrorBoundary componentDidUpdate',
684 + ] : [
685 + // Catch and render an error message
686 + 'ErrorBoundary unstable_handleError',
687 + 'ErrorBoundary render error',
688 + 'ErrorBoundary componentDidMount',
689 + ]),
690 ]);
691
692 log.length = 0;
@@ -668,16 +711,18 @@ describe('ReactErrorBoundaries', () => {
711 'ErrorBoundary render success',
712 'BrokenComponentWillMount constructor',
713 'BrokenComponentWillMount componentWillMount [!]',
671 - // Catch and render an error message
672 - 'ErrorBoundary unstable_handleError',
714 ...(ReactDOMFeatureFlags.useFiber ? [
674 - // The initial render was aborted, so
675 - // Fiber retries from the root.
676 - 'ErrorBoundary constructor',
677 - 'ErrorBoundary componentWillMount',
678 - ] : []),
679 - 'ErrorBoundary render error',
680 - 'ErrorBoundary componentDidMount',
715 + 'ErrorBoundary componentDidMount',
716 + 'ErrorBoundary unstable_handleError',
717 + 'ErrorBoundary componentWillUpdate',
718 + 'ErrorBoundary render error',
719 + 'ErrorBoundary componentDidUpdate',
720 + ] : [
721 + // Catch and render an error message
722 + 'ErrorBoundary unstable_handleError',
723 + 'ErrorBoundary render error',
724 + 'ErrorBoundary componentDidMount',
725 + ]),
726 ]);
727
728 log.length = 0;
@@ -708,21 +753,27 @@ describe('ReactErrorBoundaries', () => {
753 'BrokenRender constructor',
754 'BrokenRender componentWillMount',
755 'BrokenRender render [!]',
711 - // Handle the error:
712 - 'ErrorBoundary unstable_handleError',
756 ...(ReactDOMFeatureFlags.useFiber ? [
714 - // The initial render was aborted, so
715 - // Fiber retries from the root.
716 - 'ErrorBoundary constructor',
717 - 'ErrorBoundary componentWillMount',
718 - ] : []),
719 - 'ErrorBoundary render error',
720 - // Mount the error message:
721 - 'ErrorMessage constructor',
722 - 'ErrorMessage componentWillMount',
723 - 'ErrorMessage render',
724 - 'ErrorMessage componentDidMount',
725 - 'ErrorBoundary componentDidMount',
757 + 'ErrorBoundary componentDidMount',
758 + 'ErrorBoundary unstable_handleError',
759 + 'ErrorBoundary componentWillUpdate',
760 + 'ErrorBoundary render error',
761 + 'ErrorMessage constructor',
762 + 'ErrorMessage componentWillMount',
763 + 'ErrorMessage render',
764 + 'ErrorMessage componentDidMount',
765 + 'ErrorBoundary componentDidUpdate',
766 + ] : [
767 + // Handle the error:
768 + 'ErrorBoundary unstable_handleError',
769 + 'ErrorBoundary render error',
770 + // Mount the error message:
771 + 'ErrorMessage constructor',
772 + 'ErrorMessage componentWillMount',
773 + 'ErrorMessage render',
774 + 'ErrorMessage componentDidMount',
775 + 'ErrorBoundary componentDidMount',
776 + ]),
777 ]);
778
779 log.length = 0;
@@ -737,9 +788,9 @@ describe('ReactErrorBoundaries', () => {
788 var container = document.createElement('div');
789 ReactDOM.render(
790 <ErrorBoundary>
740 - <NoopErrorBoundary>
791 + <RethrowErrorBoundary>
792 <BrokenRender />
742 - </NoopErrorBoundary>
793 + </RethrowErrorBoundary>
794 </ErrorBoundary>,
795 container
796 );
@@ -748,39 +799,39 @@ describe('ReactErrorBoundaries', () => {
799 'ErrorBoundary constructor',
800 'ErrorBoundary componentWillMount',
801 'ErrorBoundary render success',
751 - 'NoopErrorBoundary constructor',
752 - 'NoopErrorBoundary componentWillMount',
753 - 'NoopErrorBoundary render',
754 - 'BrokenRender constructor',
755 - 'BrokenRender componentWillMount',
756 - 'BrokenRender render [!]',
757 - // The first error boundary catches the error.
758 - // However, it doesn't adjust its state so next render will also fail.
759 - 'NoopErrorBoundary unstable_handleError',
760 - ...(ReactDOMFeatureFlags.useFiber ? [
761 - // The initial render was aborted, so
762 - // Fiber retries from the root.
763 - 'ErrorBoundary constructor',
764 - 'ErrorBoundary componentWillMount',
765 - 'ErrorBoundary render success',
766 - 'NoopErrorBoundary constructor',
767 - 'NoopErrorBoundary componentWillMount',
768 - ] : []),
769 - 'NoopErrorBoundary render',
802 + 'RethrowErrorBoundary constructor',
803 + 'RethrowErrorBoundary componentWillMount',
804 + 'RethrowErrorBoundary render',
805 'BrokenRender constructor',
806 'BrokenRender componentWillMount',
807 'BrokenRender render [!]',
773 - // This time, the error propagates to the higher boundary
774 - 'ErrorBoundary unstable_handleError',
808 ...(ReactDOMFeatureFlags.useFiber ? [
776 - // The initial render was aborted, so
777 - // Fiber retries from the root.
778 - 'ErrorBoundary constructor',
779 - 'ErrorBoundary componentWillMount',
780 - ] : []),
781 - // Render the error
782 - 'ErrorBoundary render error',
783 - 'ErrorBoundary componentDidMount',
809 + // In Fiber, noop error boundaries render null
810 + 'RethrowErrorBoundary componentDidMount',
811 + 'ErrorBoundary componentDidMount',
812 + 'RethrowErrorBoundary unstable_handleError [!]',
813 + // The error got rethrown here.
814 + // This time, the error propagates to the higher boundary
815 + 'RethrowErrorBoundary componentWillUnmount',
816 + 'ErrorBoundary unstable_handleError',
817 + // Render the error
818 + 'ErrorBoundary componentWillUpdate',
819 + 'ErrorBoundary render error',
820 + 'ErrorBoundary componentDidUpdate',
821 + ] : [
822 + // The first error boundary catches the error.
823 + // However, it doesn't adjust its state so next render will also fail.
824 + 'RethrowErrorBoundary unstable_handleError [*]',
825 + 'RethrowErrorBoundary render',
826 + 'BrokenRender constructor',
827 + 'BrokenRender componentWillMount',
828 + 'BrokenRender render [!]',
829 + // This time, the error propagates to the higher boundary
830 + 'ErrorBoundary unstable_handleError',
831 + // Render the error
832 + 'ErrorBoundary render error',
833 + 'ErrorBoundary componentDidMount',
834 + ]),
835 ]);
836
837 log.length = 0;
@@ -806,16 +857,18 @@ describe('ReactErrorBoundaries', () => {
857 'BrokenComponentWillMountErrorBoundary constructor',
858 'BrokenComponentWillMountErrorBoundary componentWillMount [!]',
859 // The error propagates to the higher boundary
809 - 'ErrorBoundary unstable_handleError',
860 ...(ReactDOMFeatureFlags.useFiber ? [
811 - // The initial render was aborted, so
812 - // Fiber retries from the root.
813 - 'ErrorBoundary constructor',
814 - 'ErrorBoundary componentWillMount',
815 - ] : []),
816 - // Render the error
817 - 'ErrorBoundary render error',
818 - 'ErrorBoundary componentDidMount',
861 + 'ErrorBoundary componentDidMount',
862 + 'ErrorBoundary unstable_handleError',
863 + 'ErrorBoundary componentWillUpdate',
864 + 'ErrorBoundary render error',
865 + 'ErrorBoundary componentDidUpdate',
866 + ] : [
867 + 'ErrorBoundary unstable_handleError',
868 + // Render the error
869 + 'ErrorBoundary render error',
870 + 'ErrorBoundary componentDidMount',
871 + ]),
872 ]);
873
874 log.length = 0;
@@ -848,28 +901,29 @@ describe('ReactErrorBoundaries', () => {
901 'BrokenRender render [!]',
902 // The first error boundary catches the error
903 // It adjusts state but throws displaying the message
851 - 'BrokenRenderErrorBoundary unstable_handleError',
904 ...(ReactDOMFeatureFlags.useFiber ? [
853 - // The initial render was aborted, so
854 - // Fiber retries from the root.
855 - 'ErrorBoundary constructor',
856 - 'ErrorBoundary componentWillMount',
857 - 'ErrorBoundary render success',
858 - 'BrokenRenderErrorBoundary constructor',
859 - 'BrokenRenderErrorBoundary componentWillMount',
860 - ] : []),
861 - 'BrokenRenderErrorBoundary render error [!]',
862 - // The error propagates to the higher boundary
863 - 'ErrorBoundary unstable_handleError',
864 - ...(ReactDOMFeatureFlags.useFiber ? [
865 - // The initial render was aborted, so
866 - // Fiber retries from the root.
867 - 'ErrorBoundary constructor',
868 - 'ErrorBoundary componentWillMount',
869 - ] : []),
870 - // Render the error
871 - 'ErrorBoundary render error',
872 - 'ErrorBoundary componentDidMount',
905 + // Finish mounting with null children
906 + 'BrokenRenderErrorBoundary componentDidMount',
907 + 'ErrorBoundary componentDidMount',
908 + // Attempt to handle the error
909 + 'BrokenRenderErrorBoundary unstable_handleError',
910 + 'BrokenRenderErrorBoundary render error [!]',
911 + // Boundary fails with new error, propagate to next boundary
912 + 'BrokenRenderErrorBoundary componentWillUnmount',
913 + // Attempt to handle the error again
914 + 'ErrorBoundary unstable_handleError',
915 + 'ErrorBoundary componentWillUpdate',
916 + 'ErrorBoundary render error',
917 + 'ErrorBoundary componentDidUpdate',
918 + ] : [
919 + 'BrokenRenderErrorBoundary unstable_handleError',
920 + 'BrokenRenderErrorBoundary render error [!]',
921 + // The error propagates to the higher boundary
922 + 'ErrorBoundary unstable_handleError',
923 + // Render the error
924 + 'ErrorBoundary render error',
925 + 'ErrorBoundary componentDidMount',
926 + ]),
927 ]);
928
929 log.length = 0;
@@ -902,17 +956,21 @@ describe('ReactErrorBoundaries', () => {
956 'BrokenRender constructor',
957 'BrokenRender componentWillMount',
958 'BrokenRender render [!]',
905 - // Error boundary catches the error
906 - 'ErrorBoundary unstable_handleError',
959 ...(ReactDOMFeatureFlags.useFiber ? [
908 - // The initial render was aborted, so
909 - // Fiber retries from the root.
910 - 'ErrorBoundary constructor',
911 - 'ErrorBoundary componentWillMount',
912 - ] : []),
913 - // Render the error message
914 - 'ErrorBoundary render error',
915 - 'ErrorBoundary componentDidMount',
960 + // Finish mounting with null children
961 + 'ErrorBoundary componentDidMount',
962 + // Handle the error
963 + 'ErrorBoundary unstable_handleError',
964 + // Render the error message
965 + 'ErrorBoundary componentWillUpdate',
966 + 'ErrorBoundary render error',
967 + 'ErrorBoundary componentDidUpdate',
968 + ] : [
969 + 'ErrorBoundary unstable_handleError',
970 + // Render the error message
971 + 'ErrorBoundary render error',
972 + 'ErrorBoundary componentDidMount',
973 + ]),
974 ]);
975
976 log.length = 0;
@@ -947,21 +1005,26 @@ describe('ReactErrorBoundaries', () => {
1005 'BrokenRender componentWillMount',
1006 'BrokenRender render [!]',
1007 // Handle error:
950 - 'ErrorBoundary unstable_handleError',
1008 ...(ReactDOMFeatureFlags.useFiber ? [
952 - // The initial render was aborted, so
953 - // Fiber retries from the root.
954 - 'ErrorBoundary constructor',
955 - 'ErrorBoundary componentWillMount',
1009 + // Finish mounting with null children
1010 + 'ErrorBoundary componentDidMount',
1011 + // Handle the error
1012 + 'ErrorBoundary unstable_handleError',
1013 + // Render the error message
1014 + 'ErrorBoundary componentWillUpdate',
1015 + 'ErrorBoundary render error',
1016 + 'Error message ref is set to [object HTMLDivElement]',
1017 + 'ErrorBoundary componentDidUpdate',
1018 ] : [
1019 + 'ErrorBoundary unstable_handleError',
1020 // Stack reconciler resets ref on update, as it doesn't know ref was never set.
1021 // This is unnecessary, and Fiber doesn't do it:
1022 'Child ref is set to null',
1023 + 'ErrorBoundary render error',
1024 + // Ref to error message should get set:
1025 + 'Error message ref is set to [object HTMLDivElement]',
1026 + 'ErrorBoundary componentDidMount',
1027 ]),
961 - 'ErrorBoundary render error',
962 - // Ref to error message should get set:
963 - 'Error message ref is set to [object HTMLDivElement]',
964 - 'ErrorBoundary componentDidMount',
1028 ]);
1029
1030 log.length = 0;
@@ -1027,22 +1090,24 @@ describe('ReactErrorBoundaries', () => {
1090 'Normal2 render',
1091 // BrokenConstructor will abort rendering:
1092 'BrokenConstructor constructor [!]',
1030 - 'ErrorBoundary unstable_handleError',
1093 ...(ReactDOMFeatureFlags.useFiber ? [
1032 - // The initial render was aborted, so
1033 - // Fiber retries from the root.
1034 - 'ErrorBoundary componentWillReceiveProps',
1094 + // Finish updating with null children
1095 + 'Normal componentWillUnmount',
1096 + 'ErrorBoundary componentDidUpdate',
1097 + // Handle the error
1098 + 'ErrorBoundary unstable_handleError',
1099 + // Render the error message
1100 'ErrorBoundary componentWillUpdate',
1036 - // Fiber renders first, then unmounts in a batch:
1101 'ErrorBoundary render error',
1038 - 'Normal componentWillUnmount',
1102 + 'ErrorBoundary componentDidUpdate',
1103 ] : [
1104 + 'ErrorBoundary unstable_handleError',
1105 // Stack unmounts first, then renders:
1106 'Normal componentWillUnmount',
1107 'ErrorBoundary render error',
1108 + // Normal2 does not get lifefycle because it was never mounted
1109 + 'ErrorBoundary componentDidUpdate',
1110 ]),
1044 - // Normal2 does not get lifefycle because it was never mounted
1045 - 'ErrorBoundary componentDidUpdate',
1111 ]);
1112
1113 log.length = 0;
@@ -1085,22 +1150,24 @@ describe('ReactErrorBoundaries', () => {
1150 // BrokenComponentWillMount will abort rendering:
1151 'BrokenComponentWillMount constructor',
1152 'BrokenComponentWillMount componentWillMount [!]',
1088 - 'ErrorBoundary unstable_handleError',
1153 ...(ReactDOMFeatureFlags.useFiber ? [
1090 - // The initial render was aborted, so
1091 - // Fiber retries from the root.
1092 - 'ErrorBoundary componentWillReceiveProps',
1154 + // Finish updating with null children
1155 + 'Normal componentWillUnmount',
1156 + 'ErrorBoundary componentDidUpdate',
1157 + // Handle the error
1158 + 'ErrorBoundary unstable_handleError',
1159 + // Render the error message
1160 'ErrorBoundary componentWillUpdate',
1094 - // Fiber renders first, then unmounts in a batch:
1161 'ErrorBoundary render error',
1096 - 'Normal componentWillUnmount',
1162 + 'ErrorBoundary componentDidUpdate',
1163 ] : [
1164 + 'ErrorBoundary unstable_handleError',
1165 // Stack unmounts first, then renders:
1166 'Normal componentWillUnmount',
1167 'ErrorBoundary render error',
1168 + // Normal2 does not get lifefycle because it was never mounted
1169 + 'ErrorBoundary componentDidUpdate',
1170 ]),
1102 - // Normal2 does not get lifefycle because it was never mounted
1103 - 'ErrorBoundary componentDidUpdate',
1171 ]);
1172
1173 log.length = 0;
@@ -1138,23 +1205,24 @@ describe('ReactErrorBoundaries', () => {
1205 'Normal render',
1206 // BrokenComponentWillReceiveProps will abort rendering:
1207 'BrokenComponentWillReceiveProps componentWillReceiveProps [!]',
1141 - 'ErrorBoundary unstable_handleError',
1208 ...(ReactDOMFeatureFlags.useFiber ? [
1143 - // The initial render was aborted, so
1144 - // Fiber retries from the root.
1145 - 'ErrorBoundary componentWillReceiveProps',
1146 - 'ErrorBoundary componentWillUpdate',
1147 - // Fiber renders first, then unmounts in a batch:
1148 - 'ErrorBoundary render error',
1209 + // Finish updating with null children
1210 'Normal componentWillUnmount',
1211 'BrokenComponentWillReceiveProps componentWillUnmount',
1212 + 'ErrorBoundary componentDidUpdate',
1213 + // Handle the error
1214 + 'ErrorBoundary unstable_handleError',
1215 + 'ErrorBoundary componentWillUpdate',
1216 + 'ErrorBoundary render error',
1217 + 'ErrorBoundary componentDidUpdate',
1218 ] : [
1219 + 'ErrorBoundary unstable_handleError',
1220 // Stack unmounts first, then renders:
1221 'Normal componentWillUnmount',
1222 'BrokenComponentWillReceiveProps componentWillUnmount',
1223 'ErrorBoundary render error',
1224 + 'ErrorBoundary componentDidUpdate',
1225 ]),
1157 - 'ErrorBoundary componentDidUpdate',
1226 ]);
1227
1228 log.length = 0;
@@ -1193,23 +1261,24 @@ describe('ReactErrorBoundaries', () => {
1261 // BrokenComponentWillUpdate will abort rendering:
1262 'BrokenComponentWillUpdate componentWillReceiveProps',
1263 'BrokenComponentWillUpdate componentWillUpdate [!]',
1196 - 'ErrorBoundary unstable_handleError',
1264 ...(ReactDOMFeatureFlags.useFiber ? [
1198 - // The initial render was aborted, so
1199 - // Fiber retries from the root.
1200 - 'ErrorBoundary componentWillReceiveProps',
1201 - 'ErrorBoundary componentWillUpdate',
1202 - // Fiber renders first, then unmounts in a batch:
1203 - 'ErrorBoundary render error',
1265 + // Finish updating with null children
1266 'Normal componentWillUnmount',
1267 'BrokenComponentWillUpdate componentWillUnmount',
1268 + 'ErrorBoundary componentDidUpdate',
1269 + // Handle the error
1270 + 'ErrorBoundary unstable_handleError',
1271 + 'ErrorBoundary componentWillUpdate',
1272 + 'ErrorBoundary render error',
1273 + 'ErrorBoundary componentDidUpdate',
1274 ] : [
1275 + 'ErrorBoundary unstable_handleError',
1276 // Stack unmounts first, then renders:
1277 'Normal componentWillUnmount',
1278 'BrokenComponentWillUpdate componentWillUnmount',
1279 'ErrorBoundary render error',
1280 + 'ErrorBoundary componentDidUpdate',
1281 ]),
1212 - 'ErrorBoundary componentDidUpdate',
1282 ]);
1283
1284 log.length = 0;
@@ -1253,22 +1322,23 @@ describe('ReactErrorBoundaries', () => {
1322 'BrokenRender constructor',
1323 'BrokenRender componentWillMount',
1324 'BrokenRender render [!]',
1256 - 'ErrorBoundary unstable_handleError',
1325 ...(ReactDOMFeatureFlags.useFiber ? [
1258 - // The initial render was aborted, so
1259 - // Fiber retries from the root.
1260 - 'ErrorBoundary componentWillReceiveProps',
1326 + // Finish updating with null children
1327 + 'Normal componentWillUnmount',
1328 + 'ErrorBoundary componentDidUpdate',
1329 + // Handle the error
1330 + 'ErrorBoundary unstable_handleError',
1331 'ErrorBoundary componentWillUpdate',
1262 - // Fiber renders first, then unmounts in a batch:
1332 'ErrorBoundary render error',
1264 - 'Normal componentWillUnmount',
1333 + 'ErrorBoundary componentDidUpdate',
1334 ] : [
1335 + 'ErrorBoundary unstable_handleError',
1336 // Stack unmounts first, then renders:
1337 'Normal componentWillUnmount',
1338 'ErrorBoundary render error',
1339 + // Normal2 does not get lifefycle because it was never mounted
1340 + 'ErrorBoundary componentDidUpdate',
1341 ]),
1270 - // Normal2 does not get lifefycle because it was never mounted
1271 - 'ErrorBoundary componentDidUpdate',
1342 ]);
1343
1344 log.length = 0;
@@ -1322,16 +1392,16 @@ describe('ReactErrorBoundaries', () => {
1392 'BrokenRender constructor',
1393 'BrokenRender componentWillMount',
1394 'BrokenRender render [!]',
1325 - 'ErrorBoundary unstable_handleError',
1395 ...(ReactDOMFeatureFlags.useFiber ? [
1327 - // The initial render was aborted, so
1328 - // Fiber retries from the root.
1329 - 'ErrorBoundary componentWillReceiveProps',
1396 + // Finish updating with null children
1397 + 'Child1 ref is set to null',
1398 + 'ErrorBoundary componentDidUpdate',
1399 + // Handle the error
1400 + 'ErrorBoundary unstable_handleError',
1401 'ErrorBoundary componentWillUpdate',
1331 - // Fiber renders first, resets refs later
1402 'ErrorBoundary render error',
1333 - 'Child1 ref is set to null',
1403 ] : [
1404 + 'ErrorBoundary unstable_handleError',
1405 // Stack resets ref first, renders later
1406 'Child1 ref is set to null',
1407 'ErrorBoundary render error',
@@ -1384,7 +1454,8 @@ describe('ReactErrorBoundaries', () => {
1454 // The components have updated in this phase
1455 'BrokenComponentWillUnmount componentDidUpdate',
1456 'ErrorBoundary componentDidUpdate',
1387 - // Now that commit phase is done, Fiber handles errors
1457 + // Now that commit phase is done, Fiber unmounts the boundary's children
1458 + 'BrokenComponentWillUnmount componentWillUnmount [!]',
1459 'ErrorBoundary unstable_handleError',
1460 // The initial render was aborted, so
1461 // Fiber retries from the root.
@@ -1392,7 +1463,6 @@ describe('ReactErrorBoundaries', () => {
1463 // Render an error now (stack will do it later)
1464 'ErrorBoundary render error',
1465 // Attempt to unmount previous child:
1395 - 'BrokenComponentWillUnmount componentWillUnmount [!]',
1466 // Done
1467 'ErrorBoundary componentDidUpdate',
1468 ] : [
@@ -1454,6 +1524,8 @@ describe('ReactErrorBoundaries', () => {
1524 'BrokenComponentWillUnmount componentDidUpdate',
1525 'Normal componentDidUpdate',
1526 'ErrorBoundary componentDidUpdate',
1527 + 'Normal componentWillUnmount',
1528 + 'BrokenComponentWillUnmount componentWillUnmount [!]',
1529 // Now that commit phase is done, Fiber handles errors
1530 'ErrorBoundary unstable_handleError',
1531 // The initial render was aborted, so
@@ -1461,9 +1533,6 @@ describe('ReactErrorBoundaries', () => {
1533 'ErrorBoundary componentWillUpdate',
1534 // Render an error now (stack will do it later)
1535 'ErrorBoundary render error',
1464 - // Attempt to unmount previous child:
1465 - 'Normal componentWillUnmount',
1466 - 'BrokenComponentWillUnmount componentWillUnmount [!]',
1536 // Done
1537 'ErrorBoundary componentDidUpdate',
1538 ] : [
@@ -1803,15 +1872,16 @@ describe('ReactErrorBoundaries', () => {
1872 'LastChild componentDidMount',
1873 'ErrorBoundary componentDidMount',
1874 // Now we are ready to handle the error
1806 - 'ErrorBoundary unstable_handleError',
1807 - 'ErrorBoundary componentWillUpdate',
1808 - 'ErrorBoundary render error',
1875 // Safely unmount every child
1876 'BrokenComponentWillUnmount componentWillUnmount [!]',
1877 // Continue unmounting safely despite any errors
1878 'Normal componentWillUnmount',
1879 'BrokenComponentDidMount componentWillUnmount',
1880 'LastChild componentWillUnmount',
1881 + // Handle the error
1882 + 'ErrorBoundary unstable_handleError',
1883 + 'ErrorBoundary componentWillUpdate',
1884 + 'ErrorBoundary render error',
1885 // The update has finished
1886 'ErrorBoundary componentDidUpdate',
1887 ]);
@@ -1849,11 +1919,11 @@ describe('ReactErrorBoundaries', () => {
1919 // All lifecycles run
1920 'BrokenComponentDidUpdate componentDidUpdate [!]',
1921 'ErrorBoundary componentDidUpdate',
1922 + 'BrokenComponentDidUpdate componentWillUnmount',
1923 // Then, error is handled
1924 'ErrorBoundary unstable_handleError',
1925 'ErrorBoundary componentWillUpdate',
1926 'ErrorBoundary render error',
1856 - 'BrokenComponentDidUpdate componentWillUnmount',
1927 'ErrorBoundary componentDidUpdate',
1928 ]);
1929
@@ -1888,12 +1958,12 @@ describe('ReactErrorBoundaries', () => {
1958 'BrokenComponentDidMountErrorBoundary componentDidMount [!]',
1959 // Fiber proceeds with the hooks
1960 'ErrorBoundary componentDidMount',
1961 + 'BrokenComponentDidMountErrorBoundary componentWillUnmount',
1962 // The error propagates to the higher boundary
1963 'ErrorBoundary unstable_handleError',
1964 // Fiber retries from the root
1965 'ErrorBoundary componentWillUpdate',
1966 'ErrorBoundary render error',
1896 - 'BrokenComponentDidMountErrorBoundary componentWillUnmount',
1967 'ErrorBoundary componentDidUpdate',
1968 ]);
1969
@@ -1972,21 +2042,17 @@ describe('ReactErrorBoundaries', () => {
2042 'BrokenComponentDidUpdate componentDidUpdate [!]',
2043 'InnerUpdateBoundary componentDidUpdate',
2044 'OuterErrorBoundary componentDidUpdate',
1975 - // The interesting part starts now.
1976 - // Acknowledge errors independently but don't update yet:
2045 + // After the commit phase, attempt to recover from any errors that
2046 + // were captured
2047 'InnerUnmountBoundary unstable_handleError',
1978 - 'InnerUpdateBoundary unstable_handleError',
1979 - // Only two of four errors are acknowledged: one per boundary.
1980 - // The rest are likely cascading and we ignore them.
1981 - // Now update:
2048 'InnerUnmountBoundary componentWillUpdate',
2049 'InnerUnmountBoundary render error',
1984 - 'InnerUpdateBoundary componentWillUpdate',
1985 - 'InnerUpdateBoundary render error',
1986 - // Commit
2050 'BrokenComponentDidUpdate componentWillUnmount',
2051 'BrokenComponentDidUpdate componentWillUnmount',
2052 'InnerUnmountBoundary componentDidUpdate',
2053 + 'InnerUpdateBoundary unstable_handleError',
2054 + 'InnerUpdateBoundary componentWillUpdate',
2055 + 'InnerUpdateBoundary render error',
2056 'InnerUpdateBoundary componentDidUpdate',
2057 ]);
2058
@@ -1998,6 +2064,63 @@ describe('ReactErrorBoundaries', () => {
2064 'InnerUpdateBoundary componentWillUnmount',
2065 ]);
2066 });
2067 +
2068 + it('discards a bad root if the root component fails', () => {
2069 + spyOn(console, 'error');
2070 +
2071 + const X = null;
2072 + const Y = undefined;
2073 + let err1;
2074 + let err2;
2075 +
2076 + try {
2077 + let container = document.createElement('div');
2078 + ReactDOM.render(<X />, container);
2079 + } catch (err) {
2080 + err1 = err;
2081 + }
2082 + try {
2083 + let container = document.createElement('div');
2084 + ReactDOM.render(<Y />, container);
2085 + } catch (err) {
2086 + err2 = err;
2087 + }
2088 +
2089 + expect(err1.message).toMatch(/got: null/);
2090 + expect(err2.message).toMatch(/got: undefined/);
2091 + });
2092 +
2093 + it('renders empty output if error boundary does not handle the error', () => {
2094 + var container = document.createElement('div');
2095 + ReactDOM.render(
2096 + <div>
2097 + Sibling
2098 + <NoopErrorBoundary>
2099 + <BrokenRender />
2100 + </NoopErrorBoundary>
2101 + </div>,
2102 + container
2103 + );
2104 + expect(container.firstChild.textContent).toBe('Sibling');
2105 + expect(log).toEqual([
2106 + 'NoopErrorBoundary constructor',
2107 + 'NoopErrorBoundary componentWillMount',
2108 + 'NoopErrorBoundary render',
2109 + 'BrokenRender constructor',
2110 + 'BrokenRender componentWillMount',
2111 + 'BrokenRender render [!]',
2112 + // In Fiber, noop error boundaries render null
2113 + 'NoopErrorBoundary componentDidMount',
2114 + 'NoopErrorBoundary unstable_handleError',
2115 + // Nothing happens.
2116 + ]);
2117 +
2118 + log.length = 0;
2119 + ReactDOM.unmountComponentAtNode(container);
2120 + expect(log).toEqual([
2121 + 'NoopErrorBoundary componentWillUnmount',
2122 + ]);
2123 + });
2124 }
2125
2126 });