Handle multiple errors in boundaries
Dan Abramov committed
Oct 28, 2016 at 21:22 UTC
d8441cd725bcc3bc4f5be7d21be0f3788fde9590
2 files changed
+92
-47
src/renderers/dom/fiber/ReactDOMFiber.js
+1
@@ -131,6 +131,7 @@ var ReactDOM = {
131
render(element : ReactElement<any>, container : DOMContainerElement, callback: ?Function) {
132
warnAboutUnstableUse();
133
let root;
134
+
135
if (!container._reactRootContainer) {
136
root = container._reactRootContainer = DOMRenderer.mountContainer(element, container, callback);
137
} else {
src/renderers/shared/fiber/ReactFiberScheduler.js
+91
-47
@@ -12,6 +12,7 @@
12
13
'use strict';
14
15
+import type { TrappedError } from 'ReactFiberErrorBoundary';
16
import type { Fiber } from 'ReactFiber';
17
import type { FiberRoot } from 'ReactFiberRoot';
18
import type { HostConfig } from 'ReactFiberReconciler';
@@ -205,8 +206,7 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
206
207
// Now that the tree has been committed, we can handle errors.
208
if (allTrappedErrors) {
208
- // TODO: handle multiple errors with distinct boundaries.
209
- handleError(allTrappedErrors[0]);
209
+ handleErrors(allTrappedErrors);
210
}
211
}
212
@@ -377,7 +377,7 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
377
throw error;
378
}
379
const trappedError = trapError(failedUnitOfWork, error);
380
- handleError(trappedError);
380
+ handleErrors([trappedError]);
381
}
382
}
383
@@ -408,6 +408,7 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
408
// We're the only work scheduled.
409
nextScheduledRoot = root;
410
lastScheduledRoot = root;
411
+
412
scheduleDeferredCallback(performDeferredWork);
413
}
414
}
@@ -443,7 +444,7 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
444
throw error;
445
}
446
const trappedError = trapError(failedUnitOfWork, error);
446
- handleError(trappedError);
447
+ handleErrors([trappedError]);
448
}
449
}
450
@@ -471,56 +472,99 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
472
}
473
}
474
474
- function handleError(trappedError) {
475
- const boundary = trappedError.boundary;
476
- const error = trappedError.error;
477
- if (!boundary) {
478
- throw error;
475
+ function scheduleErrorBoundaryWork(boundary : Fiber, priority) : FiberRoot {
476
+ let root = null;
477
+ let fiber = boundary;
478
+ while (fiber) {
479
+ fiber.pendingWorkPriority = priority;
480
+ if (fiber.alternate) {
481
+ fiber.alternate.pendingWorkPriority = priority;
482
+ }
483
+ if (!fiber.return) {
484
+ if (fiber.tag === HostContainer) {
485
+ // We found the root.
486
+ // Remember it so we can update it.
487
+ root = ((fiber.stateNode : any) : FiberRoot);
488
+ break;
489
+ } else {
490
+ throw new Error('Invalid root');
491
+ }
492
+ }
493
+ fiber = fiber.return;
494
+ }
495
+ if (!root) {
496
+ throw new Error('Could not find root from the boundary.');
497
}
498
+ return root;
499
+ }
500
481
- try {
482
- // Give error boundary a chance to update its state
483
- acknowledgeErrorInBoundary(boundary, error);
484
-
485
- // We will process an update caused by an error boundary with synchronous priority.
486
- // This leaves us free to not keep track of whether a boundary has errored.
487
- // If it errors again, we will just catch the error and synchronously propagate it higher.
488
-
489
- // First, traverse upwards and set pending synchronous priority on the whole tree.
490
- let fiber = boundary;
491
- while (fiber) {
492
- fiber.pendingWorkPriority = SynchronousPriority;
493
- if (fiber.alternate) {
494
- fiber.alternate.pendingWorkPriority = SynchronousPriority;
501
+ function handleErrors(initialTrappedErrors : Array<TrappedError>) : void {
502
+ let nextTrappedErrors = initialTrappedErrors;
503
+ let firstUncaughtError = null;
504
+
505
+ // In each phase, we will attempt to pass errors to boundaries and re-render them.
506
+ // If we get more errors, we propagate them to higher boundaries in the next iterations.
507
+ while (nextTrappedErrors) {
508
+ const trappedErrors = nextTrappedErrors;
509
+ nextTrappedErrors = null;
510
+
511
+ // Pass errors to all affected boundaries.
512
+ const affectedBoundaries : Set<Fiber> = new Set();
513
+ trappedErrors.forEach(trappedError => {
514
+ const boundary = trappedError.boundary;
515
+ const error = trappedError.error;
516
+ if (!boundary) {
517
+ firstUncaughtError = firstUncaughtError || error;
518
+ return;
519
}
496
- if (!fiber.return) {
497
- if (fiber.tag === HostContainer) {
498
- // We found the root.
499
- // Now go to the second phase and update it synchronously.
500
- break;
501
- } else {
502
- throw new Error('Invalid root');
520
+ // Don't visit boundaries twice.
521
+ if (affectedBoundaries.has(boundary)) {
522
+ return;
523
+ }
524
+ // Give error boundary a chance to update its state.
525
+ try {
526
+ acknowledgeErrorInBoundary(boundary, error);
527
+ affectedBoundaries.add(boundary);
528
+ } catch (nextError) {
529
+ // If it throws, propagate the error.
530
+ nextTrappedErrors = nextTrappedErrors || [];
531
+ nextTrappedErrors.push(trapError(boundary, nextError));
532
+ }
533
+ });
534
+
535
+ // We will process an update caused by each error boundary synchronously.
536
+ affectedBoundaries.forEach(boundary => {
537
+ // FIXME: We only specify LowPriority here so that setState() calls from the error
538
+ // boundaries are respected. Instead we should set default priority level or something
539
+ // like this. Reconsider this piece when synchronous scheduling is in place.
540
+ const priority = LowPriority;
541
+ const root = scheduleErrorBoundaryWork(boundary, priority);
542
+ // This should use findNextUnitOfWork() when synchronous scheduling is implemented.
543
+ let fiber = cloneFiber(root.current, priority);
544
+ try {
545
+ while (fiber) {
546
+ // TODO: this is the only place where we recurse and it's unfortunate.
547
+ // (This may potentially get us into handleErrors() again.)
548
+ fiber = performUnitOfWork(fiber, true);
549
}
550
+ } catch (nextError) {
551
+ // If it throws, propagate the error.
552
+ nextTrappedErrors = nextTrappedErrors || [];
553
+ nextTrappedErrors.push(trapError(boundary, nextError));
554
}
505
- fiber = fiber.return;
506
- }
507
-
508
- if (!fiber) {
509
- throw new Error('Could not find an error boundary root.');
510
- }
555
+ });
556
+ }
557
512
- // Find the work in progress tree.
513
- const root : FiberRoot = (fiber.stateNode : any);
514
- fiber = root.current.alternate;
558
+ // Surface the first error uncaught by the boundaries to the user.
559
+ if (firstUncaughtError) {
560
+ // We need to make sure any future root can get scheduled despite these errors.
561
+ // Currently after throwing, nothing gets scheduled because these fields are set.
562
+ // FIXME: this is likely a wrong fix! It's still better than ignoring updates though.
563
+ nextScheduledRoot = null;
564
+ lastScheduledRoot = null;
565
516
- // Perform all the work synchronously.
517
- while (fiber) {
518
- fiber = performUnitOfWork(fiber, true);
519
- }
520
- } catch (nextError) {
521
- // Propagate error to the next boundary or rethrow.
522
- const nextTrappedError = trapError(boundary, nextError);
523
- handleError(nextTrappedError);
566
+ // Throw any unhandled errors.
567
+ throw firstUncaughtError;
568
}
569
}
570