Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Fiber] Initial error boundaries #7993

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion src/renderers/shared/fiber/ReactFiberBeginWork.js
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,12 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>, s
var ctor = workInProgress.type;
workInProgress.stateNode = instance = new ctor(props);
mount(workInProgress, instance);
state = instance.state || null;
updateQueue = workInProgress.updateQueue;
if (updateQueue) {
state = mergeUpdateQueue(updateQueue, instance.state, props);
} else {
state = null;
}
} else if (typeof instance.shouldComponentUpdate === 'function' &&
!(updateQueue && updateQueue.isForced)) {
if (workInProgress.memoizedProps !== null) {
Expand Down
4 changes: 4 additions & 0 deletions src/renderers/shared/fiber/ReactFiberClassComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ module.exports = function(scheduleUpdate : (fiber: Fiber, priorityLevel : Priori
// The instance needs access to the fiber so that it can schedule updates
ReactInstanceMap.set(instance, workInProgress);
instance.updater = updater;

if (typeof instance.componentWillMount === 'function') {
instance.componentWillMount();
}
}

return {
Expand Down
93 changes: 91 additions & 2 deletions src/renderers/shared/fiber/ReactFiberScheduler.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ var {

var {
HostContainer,
ClassComponent,
} = require('ReactTypeOfWork');

var timeHeuristicForUnitOfWork = 1;
Expand Down Expand Up @@ -285,7 +286,7 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
}
}

function performDeferredWork(deadline) {
function performDeferredWorkUnsafe(deadline) {
if (!nextUnitOfWork) {
nextUnitOfWork = findNextUnitOfWork();
}
Expand All @@ -303,6 +304,23 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
}
}

function performDeferredWork(deadline) {
try {
performDeferredWorkUnsafe(deadline);
} catch (error) {
const failedUnitOfWork = nextUnitOfWork;
// Reset because it points to the error boundary:
nextUnitOfWork = null;
if (failedUnitOfWork) {
handleError(failedUnitOfWork, error);
} else {
// We shouldn't end up here because nextUnitOfWork
// should always be set while work is being performed.
throw error;
}
}
}

function scheduleDeferredWork(root : FiberRoot, priority : PriorityLevel) {
// We must reset the current unit of work pointer so that we restart the
// search from the root during the next tick, in case there is now higher
Expand Down Expand Up @@ -334,7 +352,7 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
}
}

function performAnimationWork() {
function performAnimationWorkUnsafe() {
// Always start from the root
nextUnitOfWork = findNextUnitOfWork();
while (nextUnitOfWork &&
Expand All @@ -352,6 +370,23 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
}
}

function performAnimationWork() {
try {
performAnimationWorkUnsafe();
} catch (error) {
const failedUnitOfWork = nextUnitOfWork;
// Reset because it points to the error boundary:
nextUnitOfWork = null;
if (failedUnitOfWork) {
handleError(failedUnitOfWork, error);
} else {
// We shouldn't end up here because nextUnitOfWork
// should always be set while work is being performed.
throw error;
}
}
}

function scheduleAnimationWork(root: FiberRoot, priorityLevel : PriorityLevel) {
// Set the priority on the root, without deprioritizing
if (root.current.pendingWorkPriority === NoWork ||
Expand Down Expand Up @@ -426,6 +461,60 @@ 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) {
handleErrorInBoundary(errorBoundary, error);
return;
}
// TODO: Do we need to reset nextUnitOfWork here?
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is always safe to set nextUnitOfWork to null because it will just find its place again.

throw error;
}

function handleErrorInBoundary(errorBoundary : Fiber, error : any) {
// The work below failed so we need to clear it out and try to render the error state.
// TODO: Do we need to clear all of these fields? How do we teardown an existing tree?
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we naively just clear everything here, we'll lose the ability to unmount them. I don't think we need to clear anything here.

We normally we reset everything to whatever the "current" tree represents on the way down the tree. The simplest way might be to restart from the root.

In theory the error boundary will already be a clone, it might be fine to just restart from it. However, unstable_handleError may be calling setState on a component above (or even a sibling) and that will start screwing everything up. If you restart from the root, that case will be covered.

errorBoundary.child = null;
errorBoundary.effectTag = NoEffect;
errorBoundary.nextEffect = null;
errorBoundary.firstEffect = null;
errorBoundary.lastEffect = null;
errorBoundary.progressedPriority = NoWork;
errorBoundary.progressedChild = null;
errorBoundary.progressedFirstDeletion = null;
errorBoundary.progressedLastDeletion = null;

// Error boundary implementations would usually call setState() here:
const instance = errorBoundary.stateNode;
instance.unstable_handleError(error);

try {
// The error path should be processed synchronously.
// This lets us easily propagate errors to a parent boundary.
let unitOfWork = errorBoundary;
while (unitOfWork) {
unitOfWork = performUnitOfWork(unitOfWork);
}
} catch (nextError) {
// Propagate error to the next boundary or rethrow.
handleError(errorBoundary, nextError);
}
}

return {
scheduleWork: scheduleWork,
scheduleDeferredWork: scheduleDeferredWork,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@

'use strict';

var ReactDOMFeatureFlags = require('ReactDOMFeatureFlags');

var React;
var ReactDOM;

Expand All @@ -33,6 +35,9 @@ describe('ReactErrorBoundaries', () => {
var Normal;

beforeEach(() => {
// TODO: Fiber isn't error resilient and one test can bring down them all.
jest.resetModuleRegistry();

ReactDOM = require('ReactDOM');
React = require('React');

Expand Down Expand Up @@ -459,52 +464,93 @@ describe('ReactErrorBoundaries', () => {
};
});

// Known limitation: error boundary only "sees" errors caused by updates
// flowing through it. This might be easier to fix in Fiber.
it('currently does not catch errors originating downstream', () => {
var fail = false;
class Stateful extends React.Component {
state = {shouldThrow: false};
if (ReactDOMFeatureFlags.useFiber) {
// This test implements a new feature in Fiber.
it('catches errors originating downstream', () => {
var fail = false;
class Stateful extends React.Component {
state = {shouldThrow: false};

render() {
if (fail) {
log.push('Stateful render [!]');
throw new Error('Hello');
}
return <div />;
}
}

render() {
if (fail) {
log.push('Stateful render [!]');
throw new Error('Hello');
var statefulInst;
var container = document.createElement('div');
ReactDOM.render(
<ErrorBoundary>
<Stateful ref={inst => statefulInst = inst} />
</ErrorBoundary>,
container
);

log.length = 0;
expect(() => {
fail = true;
statefulInst.forceUpdate();
}).not.toThrow();

expect(log).toEqual([
'Stateful render [!]',
'ErrorBoundary unstable_handleError',
'ErrorBoundary render error',
'ErrorBoundary componentDidUpdate',
]);

log.length = 0;
ReactDOM.unmountComponentAtNode(container);
expect(log).toEqual([
'ErrorBoundary componentWillUnmount',
]);
});
} else {
// Known limitation: error boundary only "sees" errors caused by updates
// flowing through it. This is fixed in Fiber.
it('currently does not catch errors originating downstream', () => {
var fail = false;
class Stateful extends React.Component {
state = {shouldThrow: false};

render() {
if (fail) {
log.push('Stateful render [!]');
throw new Error('Hello');
}
return <div />;
}
return <div />;
}
}

var statefulInst;
var container = document.createElement('div');
ReactDOM.render(
<ErrorBoundary>
<Stateful ref={inst => statefulInst = inst} />
</ErrorBoundary>,
container
);
var statefulInst;
var container = document.createElement('div');
ReactDOM.render(
<ErrorBoundary>
<Stateful ref={inst => statefulInst = inst} />
</ErrorBoundary>,
container
);

log.length = 0;
expect(() => {
fail = true;
statefulInst.forceUpdate();
}).toThrow();
log.length = 0;
expect(() => {
fail = true;
statefulInst.forceUpdate();
}).toThrow();

expect(log).toEqual([
'Stateful render [!]',
// FIXME: uncomment when downstream errors get caught.
// Catch and render an error message
// 'ErrorBoundary unstable_handleError',
// 'ErrorBoundary render error',
// 'ErrorBoundary componentDidUpdate',
]);
expect(log).toEqual([
'Stateful render [!]',
]);

log.length = 0;
ReactDOM.unmountComponentAtNode(container);
expect(log).toEqual([
'ErrorBoundary componentWillUnmount',
]);
});
log.length = 0;
ReactDOM.unmountComponentAtNode(container);
expect(log).toEqual([
'ErrorBoundary componentWillUnmount',
]);
});
}

it('renders an error state if child throws in render', () => {
var container = document.createElement('div');
Expand Down Expand Up @@ -860,7 +906,7 @@ describe('ReactErrorBoundaries', () => {
'BrokenRender render [!]',
// Handle error:
'ErrorBoundary unstable_handleError',
// Child ref wasn't (and won't be) set but there's no harm in clearing:
// TODO: This is unnecessary, and Fiber doesn't do it:
'Child ref is set to null',
'ErrorBoundary render error',
// Ref to error message should get set:
Expand Down