Skip to content

Commit

Permalink
[Fiber] Initial error boundaries
Browse files Browse the repository at this point in the history
  • Loading branch information
gaearon committed Oct 17, 2016
1 parent 08da843 commit 833a7f8
Show file tree
Hide file tree
Showing 6 changed files with 144 additions and 43 deletions.
9 changes: 8 additions & 1 deletion src/renderers/shared/fiber/ReactFiber.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,11 @@ export type Fiber = Instance & {
progressedFirstDeletion: ?Fiber,
progressedLastDeletion: ?Fiber,

// This flag gets set to true for error boundaries when they attempt to
// render the error path. This way we know that if rendering fails, we should
// skip this error boundary as failed and propagate the error above.
hasErrored: boolean,

// This is a pooled version of a Fiber. Every fiber that gets updated will
// eventually have a pair. There are cases when we can clean up pairs to save
// memory if we need to.
Expand Down Expand Up @@ -194,6 +199,8 @@ var createFiber = function(tag : TypeOfWork, key : null | string) : Fiber {
progressedFirstDeletion: null,
progressedLastDeletion: null,

hasErrored: false,

alternate: null,

};
Expand Down Expand Up @@ -251,6 +258,7 @@ exports.cloneFiber = function(fiber : Fiber, priorityLevel : PriorityLevel) : Fi
alt.updateQueue = fiber.updateQueue;
alt.callbackList = fiber.callbackList;
alt.pendingWorkPriority = priorityLevel;
alt.hasErrored = fiber.hasErrored;

alt.memoizedProps = fiber.memoizedProps;
alt.memoizedState = fiber.memoizedState;
Expand Down Expand Up @@ -327,4 +335,3 @@ exports.createFiberFromYield = function(yieldNode : ReactYield, priorityLevel :
fiber.pendingProps = {};
return fiber;
};

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 (instance.state) {
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
2 changes: 2 additions & 0 deletions src/renderers/shared/fiber/ReactFiberCompleteWork.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
// Transfer update queue to callbackList field so callbacks can be
// called during commit phase.
workInProgress.callbackList = workInProgress.updateQueue;
// Reset the flag tracking whether an error boundary has failed.
workInProgress.hasErrored = false;
markUpdate(workInProgress);
return null;
case HostContainer:
Expand Down
39 changes: 38 additions & 1 deletion src/renderers/shared/fiber/ReactFiberScheduler.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ var {
} = require('ReactTypeOfSideEffect');

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

Expand Down Expand Up @@ -268,13 +269,49 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
}
}

function handleError(workInProgress, error) {
var parent = workInProgress;
while (parent = parent.return) {
if (parent.tag !== ClassComponent) {
continue;
}
if (parent.hasErrored) {
continue;
}
var instance = parent.stateNode;
if (!instance.unstable_handleError) {
continue;
}

var boundary = parent;
boundary.hasErrored = true;
boundary.child = null;
boundary.effectTag = NoEffect;
boundary.nextEffect = null;
boundary.firstEffect = null;
boundary.lastEffect = null;
boundary.progressedPriority = NoWork;
boundary.progressedChild = null;
boundary.progressedFirstDeletion = null;
boundary.progressedLastDeletion = null;
instance.unstable_handleError(error);
return boundary;
}
throw error;
}

function performUnitOfWork(workInProgress : Fiber) : ?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
// progress.
const current = workInProgress.alternate;
const next = beginWork(current, workInProgress, nextPriorityLevel);
let next = null;
try {
next = beginWork(current, workInProgress, nextPriorityLevel);
} catch (err) {
next = handleError(workInProgress, err);
}

if (next) {
// If this spawns new work, do that next.
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

0 comments on commit 833a7f8

Please sign in to comment.