Skip to content

Commit

Permalink
Catch errors in commit delete phase
Browse files Browse the repository at this point in the history
  • Loading branch information
gaearon committed Oct 27, 2016
1 parent f4db15a commit 1768e71
Show file tree
Hide file tree
Showing 3 changed files with 113 additions and 57 deletions.
75 changes: 55 additions & 20 deletions src/renderers/shared/fiber/ReactFiberCommitWork.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

'use strict';

import type { CaughtError } from 'ReactFiberErrorBoundary';
import type { Fiber } from 'ReactFiber';
import type { FiberRoot } from 'ReactFiberRoot';
import type { HostConfig } from 'ReactFiberReconciler';
Expand All @@ -23,6 +24,7 @@ var {
HostComponent,
HostText,
} = ReactTypeOfWork;
var { findClosestErrorBoundary } = require('ReactFiberErrorBoundary');
var { callCallbacks } = require('ReactFiberUpdateQueue');

var {
Expand Down Expand Up @@ -155,72 +157,94 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
}
}

function commitNestedUnmounts(root : Fiber) {
function commitNestedUnmounts(root : Fiber): Array<CaughtError> | null {
// Since errors are rare, we allocate this array on demand.
let caughtErrors = null;

// While we're inside a removed host node we don't want to call
// removeChild on the inner nodes because they're removed by the top
// call anyway. We also want to call componentWillUnmount on all
// composites before this host node is removed from the tree. Therefore
// we do an inner loop while we're still inside the host node.
let node : Fiber = root;
while (true) {
commitUnmount(node);
const error = commitUnmount(node);
if (error) {
caughtErrors = caughtErrors || [];
caughtErrors.push(error);
}
if (node.child) {
// TODO: Coroutines need to visit the stateNode.
node = node.child;
continue;
}
if (node === root) {
return;
return caughtErrors;
}
while (!node.sibling) {
if (!node.return || node.return === root) {
return;
return caughtErrors;
}
node = node.return;
}
node = node.sibling;
}
return caughtErrors;
}

function unmountHostComponents(parent, current) {
function unmountHostComponents(parent, current): Array<CaughtError> | null {
// Since errors are rare, we allocate this array on demand.
let caughtErrors = null;

// We only have the top Fiber that was inserted but we need recurse down its
// children to find all the terminal nodes.
let node : Fiber = current;
while (true) {
if (node.tag === HostComponent || node.tag === HostText) {
commitNestedUnmounts(node);
const errors = commitNestedUnmounts(node);
if (errors) {
if (!caughtErrors) {
caughtErrors = errors;
} else {
caughtErrors.push.apply(caughtErrors, errors);
}
}
// After all the children have unmounted, it is now safe to remove the
// node from the tree.
if (parent) {
removeChild(parent, node.stateNode);
}
} else {
commitUnmount(node);
const error = commitUnmount(node);
if (error) {
caughtErrors = caughtErrors || [];
caughtErrors.push(error);
}
if (node.child) {
// TODO: Coroutines need to visit the stateNode.
node = node.child;
continue;
}
}
if (node === current) {
return;
return caughtErrors;
}
while (!node.sibling) {
if (!node.return || node.return === current) {
return;
return caughtErrors;
}
node = node.return;
}
node = node.sibling;
}
return caughtErrors;
}

function commitDeletion(current : Fiber) : void {
function commitDeletion(current : Fiber) : Array<CaughtError> | null {
// Recursively delete all host nodes from the parent.
// TODO: Error handling.
const parent = getHostParent(current);

unmountHostComponents(parent, current);
// Detach refs and call componentWillUnmount() on the whole subtree.
const caughtErrors = unmountHostComponents(parent, current);

// Cut off the return pointers to disconnect it from the tree. Ideally, we
// should clear the child pointer of the parent alternate to let this
Expand All @@ -233,31 +257,42 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
current.alternate.child = null;
current.alternate.return = null;
}

return caughtErrors;
}

function commitUnmount(current : Fiber) : void {
function commitUnmount(current : Fiber) : CaughtError | null {
switch (current.tag) {
case ClassComponent: {
detachRef(current);
const instance = current.stateNode;
if (typeof instance.componentWillUnmount === 'function') {
instance.componentWillUnmount();
const error = tryCallComponentWillUnmount(instance);
if (error) {
return {
error,
boundary: findClosestErrorBoundary(current),
};
}
}
return;
return null;
}
case HostComponent: {
detachRef(current);
return;
return null;
}
default: {
return null;
}
}
}

function callComponentWillUnmountAndIgnoreErrors(instance) {
function tryCallComponentWillUnmount(instance) {
try {
instance.componentWillUnmount();
return null;
} catch (error) {
// Ignore any errors because we are already cleaning up
// due to another error that is being handled by a boundary.
return error;
}
}

Expand Down
71 changes: 41 additions & 30 deletions src/renderers/shared/fiber/ReactFiberScheduler.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ var ReactFiberCommitWork = require('ReactFiberCommitWork');
var ReactCurrentOwner = require('ReactCurrentOwner');

var { cloneFiber } = require('ReactFiber');
var { findClosestErrorBoundary, sendErrorToBoundary } = require('ReactFiberErrorBoundary');

var {
NoWork,
Expand Down Expand Up @@ -110,9 +111,15 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
return null;
}

function commitAllWork(finishedWork : Fiber) {
function commitAllWork(finishedWork : Fiber, ignoreUnmountingErrors : boolean) {
// Commit all the side-effects within a tree.

// Commit phase is meant to be atomic and non-interruptible.
// Any errors raised in it should be handled after it is over
// so that we don't end up in an inconsistent state due to user code.
// We'll keep track of all caught errors and handle them later.
let caughtErrors = null;

// First, we'll perform all the host insertions, updates, deletions and
// ref unmounts.
let effectfulFiber = finishedWork.firstEffect;
Expand Down Expand Up @@ -140,7 +147,20 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
break;
}
case Deletion: {
commitDeletion(effectfulFiber);
// Deletion might cause an error in componentWillUnmount().
// We will continue nevertheless and handle those later on.
const errors = commitDeletion(effectfulFiber);
// There is a special case where we completely ignore errors.
// It happens when we already caught an error earlier, and the update
// is caused by an error boundary trying to render an error message.
// In this case, we want to blow away the tree without catching errors.
if (errors && !ignoreUnmountingErrors) {
if (!caughtErrors) {
caughtErrors = errors;
} else {
caughtErrors.push.apply(caughtErrors, errors);
}
}
break;
}
}
Expand Down Expand Up @@ -175,6 +195,16 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
commitWork(current, finishedWork);
commitLifeCycles(current, finishedWork);
}

// Now that the tree has been committed, we can handle errors.
if (caughtErrors) {
// TODO: handle multiple errors
if (caughtErrors[0].boundary) {
handleErrorInBoundary(caughtErrors[0].boundary, caughtErrors[0].error);
} else {
throw caughtErrors[0].error;
}
}
}

function resetWorkPriority(workInProgress : Fiber) {
Expand All @@ -195,7 +225,7 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
workInProgress.pendingWorkPriority = newPriority;
}

function completeUnitOfWork(workInProgress : Fiber) : ?Fiber {
function completeUnitOfWork(workInProgress : Fiber, ignoreUnmountingErrors : boolean) : ?Fiber {
while (true) {
// The current, flushed, state of this fiber is the alternate.
// Ideally nothing should rely on this, but relying on it here
Expand Down Expand Up @@ -262,14 +292,12 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
'related to the return field.'
);
}
root.current = workInProgress;
// TODO: We can be smarter here and only look for more work in the
// "next" scheduled work since we've already scanned passed. That
// also ensures that work scheduled during reconciliation gets deferred.
// const hasMoreWork = workInProgress.pendingWorkPriority !== NoWork;
commitAllWork(workInProgress);
// Swap the pointer after committing all work so that if committing fails,
// we still treat it as a work in progress in case there is an error boundary.
root.current = workInProgress;
commitAllWork(workInProgress, ignoreUnmountingErrors);
const nextWork = findNextUnitOfWork();
// if (!nextWork && hasMoreWork) {
// TODO: This can happen when some deep work completes and we don't
Expand All @@ -283,7 +311,7 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
}
}

function performUnitOfWork(workInProgress : Fiber) : ?Fiber {
function performUnitOfWork(workInProgress : Fiber, ignoreUnmountingErrors : boolean) : ?Fiber {
// The current, flushed, state of this fiber is the alternate.
// Ideally nothing should rely on this, but relying on it here
// means that we don't need an additional field on the work in
Expand All @@ -304,7 +332,7 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
ReactFiberInstrumentation.debugTool.onWillCompleteWork(workInProgress);
}
// If this doesn't spawn new work, complete the current work.
next = completeUnitOfWork(workInProgress);
next = completeUnitOfWork(workInProgress, ignoreUnmountingErrors);
if (__DEV__ && ReactFiberInstrumentation.debugTool) {
ReactFiberInstrumentation.debugTool.onDidCompleteWork(workInProgress);
}
Expand All @@ -321,7 +349,7 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
}
while (nextUnitOfWork) {
if (deadline.timeRemaining() > timeHeuristicForUnitOfWork) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
nextUnitOfWork = performUnitOfWork(nextUnitOfWork, false);
if (!nextUnitOfWork) {
// Find more work. We might have time to complete some more.
nextUnitOfWork = findNextUnitOfWork();
Expand Down Expand Up @@ -386,7 +414,7 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
nextUnitOfWork = findNextUnitOfWork();
while (nextUnitOfWork &&
nextPriorityLevel !== NoWork) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
nextUnitOfWork = performUnitOfWork(nextUnitOfWork, false);
if (!nextUnitOfWork) {
// Keep searching for animation work until there's no more left
nextUnitOfWork = findNextUnitOfWork();
Expand Down Expand Up @@ -440,20 +468,6 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
}
}

function findClosestErrorBoundary(fiber : Fiber): ?Fiber {
let maybeErrorBoundary = fiber.return;
while (maybeErrorBoundary) {
if (maybeErrorBoundary.tag === ClassComponent) {
const instance = maybeErrorBoundary.stateNode;
if (typeof instance.unstable_handleError === 'function') {
return maybeErrorBoundary;
}
}
maybeErrorBoundary = maybeErrorBoundary.return;
}
return null;
}

function handleError(failedUnitOfWork : Fiber, error : any) {
const errorBoundary = findClosestErrorBoundary(failedUnitOfWork);
if (errorBoundary) {
Expand All @@ -465,10 +479,7 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {

function handleErrorInBoundary(errorBoundary : Fiber, error : any) {
try {
// Error boundary implementations would usually call setState() here:
const instance = errorBoundary.stateNode;
instance.unstable_handleError(error);

sendErrorToBoundary(errorBoundary, error);
// We will process an update caused by an error boundary with synchronous priority.
// This leaves us free to not keep track of whether a boundary has errored.
// If it errors again, we will just catch the error and synchronously propagate it higher.
Expand All @@ -493,7 +504,7 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
}
// Restart work from the root and try to re-render the errored tree.
while (fiber) {
fiber = performUnitOfWork(fiber);
fiber = performUnitOfWork(fiber, true);
}
} catch (nextError) {
// Propagate error to the next boundary or rethrow.
Expand Down
Loading

0 comments on commit 1768e71

Please sign in to comment.