@samitouri / QOS-React / commits / d8441cd725

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