Skip to content

Commit

Permalink
Merge pull request #8961 from acdlite/fiberbreakonuncaught
Browse files Browse the repository at this point in the history
[Fiber] Preserve "Break on all/uncaught exceptions" behavior in DEV mode
  • Loading branch information
acdlite committed Feb 25, 2017
2 parents 0934ab9 + 2c59713 commit 07dc04d
Show file tree
Hide file tree
Showing 8 changed files with 237 additions and 122 deletions.
19 changes: 14 additions & 5 deletions scripts/fiber/tests-passing.txt
Expand Up @@ -1667,11 +1667,20 @@ src/renderers/shared/stack/reconciler/__tests__/Transaction-test.js
* should allow nesting of transactions

src/renderers/shared/utils/__tests__/ReactErrorUtils-test.js
* should call the callback with only the passed argument
* should catch errors
* should rethrow caught errors
* should call the callback with only the passed argument
* should use invokeGuardedCallbackWithCatch in production
* it should rethrow errors caught by invokeGuardedCallbackAndCatchFirstError (development)
* should call the callback the passed arguments (development)
* should call the callback with the provided context (development)
* should return a caught error (development)
* should return null if no error is thrown (development)
* can nest with same debug name (development)
* does not return nested errors (development)
* it should rethrow errors caught by invokeGuardedCallbackAndCatchFirstError (production)
* should call the callback the passed arguments (production)
* should call the callback with the provided context (production)
* should return a caught error (production)
* should return null if no error is thrown (production)
* can nest with same debug name (production)
* does not return nested errors (production)

src/renderers/shared/utils/__tests__/accumulateInto-test.js
* throws if the second item is null
Expand Down
7 changes: 0 additions & 7 deletions src/renderers/native/__tests__/ReactNativeEvents-test.js
Expand Up @@ -13,7 +13,6 @@

var RCTEventEmitter;
var React;
var ReactErrorUtils;
var ReactNative;
var ResponderEventPlugin;
var UIManager;
Expand All @@ -24,16 +23,10 @@ beforeEach(() => {

RCTEventEmitter = require('RCTEventEmitter');
React = require('React');
ReactErrorUtils = require('ReactErrorUtils');
ReactNative = require('ReactNative');
ResponderEventPlugin = require('ResponderEventPlugin');
UIManager = require('UIManager');
createReactNativeComponentClass = require('createReactNativeComponentClass');

// Ensure errors from event callbacks are properly surfaced (otherwise,
// jest/jsdom swallows them when we do the .dispatchEvent call)
ReactErrorUtils.invokeGuardedCallback =
ReactErrorUtils.invokeGuardedCallbackWithCatch;
});

it('handles events', () => {
Expand Down
36 changes: 25 additions & 11 deletions src/renderers/shared/fiber/ReactFiberCommitWork.js
Expand Up @@ -26,6 +26,7 @@ var {
} = ReactTypeOfWork;
var { commitCallbacks } = require('ReactFiberUpdateQueue');
var { onCommitUnmount } = require('ReactFiberDevToolsHook');
var { invokeGuardedCallback } = require('ReactErrorUtils');

var {
Placement,
Expand Down Expand Up @@ -54,22 +55,35 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(

// Capture errors so they don't interrupt unmounting.
function safelyCallComponentWillUnmount(current, instance) {
try {
instance.componentWillUnmount();
} catch (error) {
captureError(current, error);
if (__DEV__) {
const unmountError = invokeGuardedCallback(null, instance.componentWillUnmount, instance);
if (unmountError) {
captureError(current, unmountError);
}
} else {
try {
instance.componentWillUnmount();
} catch (unmountError) {
captureError(current, unmountError);
}
}
}

// Capture errors so they don't interrupt unmounting.
function safelyDetachRef(current : Fiber) {
try {
const ref = current.ref;
if (ref !== null) {
ref(null);
const ref = current.ref;
if (ref !== null) {
if (__DEV__) {
const refError = invokeGuardedCallback(null, ref, null, null);
if (refError !== null) {
captureError(current, refError);
}
} else {
try {
ref(null);
} catch (refError) {
captureError(current, refError);
}
}
} catch (error) {
captureError(current, error);
}
}

Expand Down
63 changes: 43 additions & 20 deletions src/renderers/shared/fiber/ReactFiberScheduler.js
Expand Up @@ -35,6 +35,7 @@ var {
getStackAddendumByWorkInProgressFiber,
} = require('ReactComponentTreeHook');
var { logCapturedError } = require('ReactFiberErrorLogger');
var { invokeGuardedCallback } = require('ReactErrorUtils');

var ReactFiberBeginWork = require('ReactFiberBeginWork');
var ReactFiberCompleteWork = require('ReactFiberCompleteWork');
Expand Down Expand Up @@ -164,6 +165,9 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(config : HostConfig<T, P,
// Keeps track of whether we're currently in a work loop.
let isPerformingWork : boolean = false;

// Keeps track of whether the current deadline has expired.
let deadlineHasExpired : boolean = false;

// Keeps track of whether we should should batch sync updates.
let isBatchingUpdates : boolean = false;

Expand Down Expand Up @@ -414,9 +418,17 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(config : HostConfig<T, P,
// ref unmounts.
nextEffect = firstEffect;
while (nextEffect !== null) {
try {
commitAllHostEffects(finishedWork);
} catch (error) {
let error = null;
if (__DEV__) {
error = invokeGuardedCallback(null, commitAllHostEffects, null, finishedWork);
} else {
try {
commitAllHostEffects(finishedWork);
} catch (e) {
error = e;
}
}
if (error !== null) {
invariant(
nextEffect !== null,
'Should have next effect. This error is likely caused by a bug ' +
Expand Down Expand Up @@ -444,9 +456,17 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(config : HostConfig<T, P,
// This pass also triggers any renderer-specific initial effects.
nextEffect = firstEffect;
while (nextEffect !== null) {
try {
commitAllLifeCycles(finishedWork, nextEffect);
} catch (error) {
let error = null;
if (__DEV__) {
error = invokeGuardedCallback(null, commitAllLifeCycles, null, finishedWork);
} else {
try {
commitAllLifeCycles(finishedWork);
} catch (e) {
error = e;
}
}
if (error !== null) {
invariant(
nextEffect !== null,
'Should have next effect. This error is likely caused by a bug ' +
Expand Down Expand Up @@ -675,7 +695,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(config : HostConfig<T, P,
}
}

function workLoop(priorityLevel, deadline : Deadline | null, deadlineHasExpired : boolean) : boolean {
function workLoop(priorityLevel, deadline : Deadline | null) {
// Clear any errors.
clearErrors();

Expand Down Expand Up @@ -743,8 +763,6 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(config : HostConfig<T, P,
if (hostRootTimeMarker) {
console.timeEnd(hostRootTimeMarker);
}

return deadlineHasExpired;
}

function performWork(priorityLevel : PriorityLevel, deadline : Deadline | null) {
Expand All @@ -755,7 +773,6 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(config : HostConfig<T, P,
);
isPerformingWork = true;
const isPerformingDeferredWork = Boolean(deadline);
let deadlineHasExpired = false;

// This outer loop exists so that we can restart the work loop after
// catching an error. It also lets us flush Task work at the end of a
Expand All @@ -776,18 +793,25 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(config : HostConfig<T, P,
// Nothing in performWork should be allowed to throw. All unsafe
// operations must happen within workLoop, which is extracted to a
// separate function so that it can be optimized by the JS engine.
try {
priorityContextBeforeReconciliation = priorityContext;
priorityContext = nextPriorityLevel;
deadlineHasExpired = workLoop(priorityLevel, deadline, deadlineHasExpired);
} catch (error) {
priorityContextBeforeReconciliation = priorityContext;
let error = null;
if (__DEV__) {
error = invokeGuardedCallback(null, workLoop, null, priorityLevel, deadline);
} else {
try {
workLoop(priorityLevel, deadline);
} catch (e) {
error = e;
}
}
// Reset the priority context to its value before reconcilation.
priorityContext = priorityContextBeforeReconciliation;

if (error !== null) {
// We caught an error during either the begin or complete phases.
const failedWork = nextUnitOfWork;

if (failedWork !== null) {
// Reset the priority context to its value before reconciliation.
priorityContext = priorityContextBeforeReconciliation;

// "Capture" the error by finding the nearest boundary. If there is no
// error boundary, the nearest host container acts as one. If
// captureError returns null, the error was intentionally ignored.
Expand Down Expand Up @@ -818,8 +842,6 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(config : HostConfig<T, P,
// inside resetAfterCommit.
fatalError = error;
}
} finally {
priorityContext = priorityContextBeforeReconciliation;
}

// Stop performing work
Expand Down Expand Up @@ -862,6 +884,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(config : HostConfig<T, P,

// We're done performing work. Time to clean up.
isPerformingWork = false;
deadlineHasExpired = false;
fatalError = null;
firstUncaughtError = null;
capturedErrors = null;
Expand Down
10 changes: 1 addition & 9 deletions src/renderers/shared/shared/event/EventPluginUtils.js
Expand Up @@ -89,15 +89,7 @@ if (__DEV__) {
function executeDispatch(event, simulated, listener, inst) {
var type = event.type || 'unknown-event';
event.currentTarget = EventPluginUtils.getNodeFromInstance(inst);
if (simulated) {
ReactErrorUtils.invokeGuardedCallbackWithCatch(
type,
listener,
event
);
} else {
ReactErrorUtils.invokeGuardedCallback(type, listener, event);
}
ReactErrorUtils.invokeGuardedCallbackAndCatchFirstError(type, listener, undefined, event);
event.currentTarget = null;
}

Expand Down
Expand Up @@ -557,7 +557,7 @@ var ReactCompositeComponent = {
if (safely) {
if (!skipLifecycle) {
var name = this.getName() + '.componentWillUnmount()';
ReactErrorUtils.invokeGuardedCallback(name, inst.componentWillUnmount.bind(inst));
ReactErrorUtils.invokeGuardedCallbackAndCatchFirstError(name, inst.componentWillUnmount, inst);
}
} else {
if (__DEV__) {
Expand Down

0 comments on commit 07dc04d

Please sign in to comment.