Skip to content
Merged
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
4 changes: 4 additions & 0 deletions scripts/fiber/tests-passing.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1194,6 +1194,10 @@ src/renderers/shared/fiber/__tests__/ReactIncrementalErrorHandling-test.js
* does not interrupt unmounting if detaching a ref throws
* handles error thrown by host config while working on failed root
* handles error thrown by top-level callback
* should log errors that occur during the begin phase
* should log errors that occur during the commit phase
* should ignore errors thrown in log method to prevent cycle
* should relay info about error boundary and retry attempts if applicable

src/renderers/shared/fiber/__tests__/ReactIncrementalReflection-test.js
* handles isMounted even when the initial render is deferred
Expand Down
4 changes: 4 additions & 0 deletions scripts/jest/test-framework-setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ jest.mock('ReactDOMFeatureFlags', () => {
});
});

// Error logging varies between Fiber and Stack;
// Rather than fork dozens of tests, mock the error-logging file by default.
jest.mock('ReactFiberErrorLogger');

var env = jasmine.getEnv();

var callCount = 0;
Expand Down
66 changes: 66 additions & 0 deletions src/renderers/shared/fiber/ReactFiberErrorLogger.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/**
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule ReactFiberErrorLogger
* @flow
*/

'use strict';

import type { CapturedError } from 'ReactFiberScheduler';

function logCapturedError(capturedError : CapturedError) : void {
if (__DEV__) {
const {
componentName,
componentStack,
error,
errorBoundaryName,
errorBoundaryFound,
willRetry,
} = capturedError;

const componentNameMessage = componentName
? `React caught an error thrown by ${componentName}.`
: 'React caught an error thrown by one of your components.';

let errorBoundaryMessage;
// errorBoundaryFound check is sufficient; errorBoundaryName check is to satisfy Flow.
if (errorBoundaryFound && errorBoundaryName) {
if (willRetry) {
errorBoundaryMessage =
`React will try to recreate this component tree from scratch ` +
`using the error boundary you provided, ${errorBoundaryName}.`;
} else {
errorBoundaryMessage =
`This error was initially handled by the error boundary ${errorBoundaryName}. ` +
`Recreating the tree from scratch failed so React will unmount the tree.`;
}
} else {
// TODO Link to unstable_handleError() documentation once it exists.
errorBoundaryMessage =
'Consider adding an error boundary to your tree to customize error handling behavior.';
}

console.error(
`${componentNameMessage} You should fix this error in your code. ${errorBoundaryMessage}\n\n` +
`${error.stack}\n\n` +
`The error was thrown in the following location: ${componentStack}`
);
}

if (!__DEV__) {
const { error } = capturedError;
console.error(
`React caught an error thrown by one of your components.\n\n${error.stack}`
);
}
}

exports.logCapturedError = logCapturedError;

73 changes: 63 additions & 10 deletions src/renderers/shared/fiber/ReactFiberScheduler.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,30 @@ import type { FiberRoot } from 'ReactFiberRoot';
import type { HostConfig, Deadline } from 'ReactFiberReconciler';
import type { PriorityLevel } from 'ReactPriorityLevel';

export type CapturedError = {
componentName : ?string,
componentStack : string,
error : Error,
errorBoundaryFound : boolean,
errorBoundaryName : ?string,
willRetry : boolean,
};

var {
popContextProvider,
} = require('ReactFiberContext');
const { reset } = require('ReactFiberStack');
var {
getStackAddendumByWorkInProgressFiber,
} = require('ReactComponentTreeHook');
var { logCapturedError } = require('ReactFiberErrorLogger');

var ReactFiberBeginWork = require('ReactFiberBeginWork');
var ReactFiberCompleteWork = require('ReactFiberCompleteWork');
var ReactFiberCommitWork = require('ReactFiberCommitWork');
var ReactFiberHostContext = require('ReactFiberHostContext');
var ReactCurrentOwner = require('ReactCurrentOwner');
var getComponentName = require('getComponentName');

var { cloneFiber } = require('ReactFiber');

Expand Down Expand Up @@ -134,7 +148,7 @@ module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C

// Keep track of which fibers have captured an error that need to be handled.
// Work is removed from this collection after unstable_handleError is called.
let capturedErrors : Map<Fiber, Error> | null = null;
let capturedErrors : Map<Fiber, CapturedError> | null = null;
// Keep track of which fibers have failed during the current batch of work.
// This is a different set than capturedErrors, because it is not reset until
// the end of the batch. This is needed to propagate errors correctly if a
Expand Down Expand Up @@ -814,6 +828,12 @@ module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C

// Search for the nearest error boundary.
let boundary : ?Fiber = null;

// Passed to logCapturedError()
let errorBoundaryFound : boolean = false;
let willRetry : boolean = false;
let errorBoundaryName : ?string = null;

// Host containers are a special case. If the failed work itself is a host
// container, then it acts as its own boundary. In all other cases, we
// ignore the work itself and only search through the parents.
Expand All @@ -832,6 +852,9 @@ module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C
if (node.tag === ClassComponent) {
const instance = node.stateNode;
if (typeof instance.unstable_handleError === 'function') {
errorBoundaryFound = true;
errorBoundaryName = getComponentName(node);

if (isFailedBoundary(node)) {
// This boundary is already in a failed state. The error should
// propagate to the next boundary — except in the
Expand All @@ -858,6 +881,7 @@ module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C
} else {
// Found an error boundary!
boundary = node;
willRetry = true;
}
}
} else if (node.tag === HostRoot) {
Expand All @@ -876,15 +900,29 @@ module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C
}
failedBoundaries.add(boundary);

// This method is unsafe outside of the begin and complete phases.
// We might be in the commit phase when an error is captured.
// The risk is that the return path from this Fiber may not be accurate.
// That risk is acceptable given the benefit of providing users more context.
const componentStack = getStackAddendumByWorkInProgressFiber(failedWork);
const componentName = getComponentName(failedWork);

// Add to the collection of captured errors. This is stored as a global
// map of errors keyed by the boundaries that capture them. We mostly
// use this Map as a Set; it's a Map only to avoid adding a field to Fiber
// to store the error.
// map of errors and their component stack location keyed by the boundaries
// that capture them. We mostly use this Map as a Set; it's a Map only to
// avoid adding a field to Fiber to store the error.
if (!capturedErrors) {
capturedErrors = new Map();
}
capturedErrors.set(boundary, {
componentName,
componentStack,
error,
errorBoundaryFound,
errorBoundaryName,
willRetry,
});

capturedErrors.set(boundary, error);
// If we're in the commit phase, defer scheduling an update on the
// boundary until after the commit is complete
if (isCommitting) {
Expand Down Expand Up @@ -923,23 +961,38 @@ module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C
}

function commitErrorHandling(effectfulFiber : Fiber) {
let error;
let capturedError;
if (capturedErrors) {
error = capturedErrors.get(effectfulFiber);
capturedError = capturedErrors.get(effectfulFiber);
capturedErrors.delete(effectfulFiber);
if (!error) {
if (!capturedError) {
if (effectfulFiber.alternate) {
effectfulFiber = effectfulFiber.alternate;
error = capturedErrors.get(effectfulFiber);
capturedError = capturedErrors.get(effectfulFiber);
capturedErrors.delete(effectfulFiber);
}
}
}

if (!error) {
if (!capturedError) {
throw new Error('No error for given unit of work.');
}

let error;

// Conditional required to satisfy Flow
if (capturedError) {
error = capturedError.error;

try {
logCapturedError(capturedError);
} catch (e) {
// Prevent cycle if logCapturedError() throws.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can we console.error once here too?

// A cycle may still occur if logCapturedError renders a component that throws.
console.error(e);
}
}

switch (effectfulFiber.tag) {
case ClassComponent:
const instance = effectfulFiber.stateNode;
Expand Down
Loading