mirror of
https://github.com/zebrajr/react.git
synced 2026-01-15 12:15:22 +00:00
Support throwing null (#10213)
* Support throwing null In JavaScript, you can throw values of any type, not just errors. That includes null. We currently rely on null checks to determine if a user- provided function has thrown. This refactors our error handling code to keep track of an explicit boolean flag instead. * Add DOM fixture test case for break on exception behavior * preventDefault error events during feature test We call invokeGuardedCallbackDev at startup as part of a feature test. But we don't want those errors to log to the console. * Add throwing null test case * Use ReactFeatureFlags instead of ReactDOMFeatureFlags React ART uses this, too. * Non-errors in error logger If a non-error is thrown, we'll coerce the value to a string and use that as the message.
This commit is contained in:
@@ -59,6 +59,7 @@ class Header extends React.Component {
|
||||
Input change events
|
||||
</option>
|
||||
<option value="/buttons">Buttons</option>
|
||||
<option value="/error-handling">Error Handling</option>
|
||||
</select>
|
||||
</label>
|
||||
<label htmlFor="react_version">
|
||||
|
||||
88
fixtures/dom/src/components/fixtures/error-handling/index.js
Normal file
88
fixtures/dom/src/components/fixtures/error-handling/index.js
Normal file
@@ -0,0 +1,88 @@
|
||||
const React = window.React;
|
||||
|
||||
import FixtureSet from '../../FixtureSet';
|
||||
import TestCase from '../../TestCase';
|
||||
|
||||
function BadRender(props) {
|
||||
throw props.error;
|
||||
}
|
||||
class ErrorBoundary extends React.Component {
|
||||
state = {
|
||||
shouldThrow: false,
|
||||
didThrow: false,
|
||||
error: null,
|
||||
};
|
||||
componentDidCatch(error) {
|
||||
this.setState({error, didThrow: true});
|
||||
}
|
||||
triggerError = () => {
|
||||
this.setState({
|
||||
shouldThrow: true,
|
||||
});
|
||||
};
|
||||
render() {
|
||||
if (this.state.didThrow) {
|
||||
if (this.state.error) {
|
||||
return `Captured an error: ${this.state.error.message}`;
|
||||
} else {
|
||||
return `Captured an error: ${this.state.error}`;
|
||||
}
|
||||
}
|
||||
if (this.state.shouldThrow) {
|
||||
return <BadRender error={this.props.error} />;
|
||||
}
|
||||
return <button onClick={this.triggerError}>Trigger error</button>;
|
||||
}
|
||||
}
|
||||
class Example extends React.Component {
|
||||
state = {key: 0};
|
||||
restart = () => {
|
||||
this.setState(state => ({key: state.key + 1}));
|
||||
};
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<ErrorBoundary error={this.props.error} key={this.state.key} />
|
||||
<button onClick={this.restart}>Reset</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default class ErrorHandlingTestCases extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<FixtureSet title="Error handling" description="">
|
||||
<TestCase
|
||||
title="Break on uncaught exceptions"
|
||||
description="In DEV, errors should be treated as uncaught, even though React catches them internally">
|
||||
<TestCase.Steps>
|
||||
<li>Open the browser DevTools</li>
|
||||
<li>Make sure "Pause on exceptions" is enabled</li>
|
||||
<li>Make sure "Pause on caught exceptions" is disabled</li>
|
||||
<li>Click the "Trigger error" button</li>
|
||||
<li>Click the reset button</li>
|
||||
</TestCase.Steps>
|
||||
<TestCase.ExpectedResult>
|
||||
The DevTools should pause at the line where the error was thrown, in
|
||||
the BadRender component. After resuming, the "Trigger error" button
|
||||
should be replaced with "Captured an error: Oops!" Clicking reset
|
||||
should reset the test case.
|
||||
</TestCase.ExpectedResult>
|
||||
<Example error={new Error('Oops!')} />
|
||||
</TestCase>
|
||||
<TestCase title="Throwing null" description="">
|
||||
<TestCase.Steps>
|
||||
<li>Click the "Trigger error" button</li>
|
||||
<li>Click the reset button</li>
|
||||
</TestCase.Steps>
|
||||
<TestCase.ExpectedResult>
|
||||
The "Trigger error" button should be replaced with "Captured an
|
||||
error: null". Clicking reset should reset the test case.
|
||||
</TestCase.ExpectedResult>
|
||||
<Example error={null} />
|
||||
</TestCase>
|
||||
</FixtureSet>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import InputChangeEvents from './input-change-events';
|
||||
import NumberInputFixtures from './number-inputs';
|
||||
import PasswordInputFixtures from './password-inputs';
|
||||
import ButtonFixtures from './buttons';
|
||||
import ErrorHandling from './error-handling';
|
||||
|
||||
/**
|
||||
* A simple routing component that renders the appropriate
|
||||
@@ -30,6 +31,8 @@ function FixturesPage() {
|
||||
return <PasswordInputFixtures />;
|
||||
case '/buttons':
|
||||
return <ButtonFixtures />;
|
||||
case '/error-handling':
|
||||
return <ErrorHandling />;
|
||||
default:
|
||||
return <p>Please select a test fixture.</p>;
|
||||
}
|
||||
|
||||
@@ -2328,21 +2328,22 @@ src/renderers/shared/stack/reconciler/__tests__/Transaction-test.js
|
||||
* should allow nesting of transactions
|
||||
|
||||
src/renderers/shared/utils/__tests__/ReactErrorUtils-test.js
|
||||
* it should rethrow errors caught by invokeGuardedCallbackAndCatchFirstError (development)
|
||||
* it should rethrow caught errors (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)
|
||||
* should catch errors (development)
|
||||
* should return false from clearCaughtError if no error was thrown (development)
|
||||
* can nest with same debug name (development)
|
||||
* does not return nested errors (development)
|
||||
* can be shimmed (development)
|
||||
* it should rethrow errors caught by invokeGuardedCallbackAndCatchFirstError (production)
|
||||
* it should rethrow caught errors (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)
|
||||
* should catch errors (production)
|
||||
* should return false from clearCaughtError if no error was thrown (production)
|
||||
* can nest with same debug name (production)
|
||||
* does not return nested errors (production)
|
||||
* catches null values
|
||||
* can be shimmed (production)
|
||||
|
||||
src/renderers/shared/utils/__tests__/accumulateInto-test.js
|
||||
|
||||
@@ -12,6 +12,7 @@ jest.mock('ReactFeatureFlags', () => {
|
||||
const flags = require.requireActual('ReactFeatureFlags');
|
||||
return Object.assign({}, flags, {
|
||||
disableNewFiberFeatures: true,
|
||||
forceInvokeGuardedCallbackDev: true,
|
||||
});
|
||||
});
|
||||
jest.mock('ReactNativeFeatureFlags', () => {
|
||||
|
||||
@@ -26,7 +26,11 @@ var {
|
||||
} = ReactTypeOfWork;
|
||||
var {commitCallbacks} = require('ReactFiberUpdateQueue');
|
||||
var {onCommitUnmount} = require('ReactFiberDevToolsHook');
|
||||
var {invokeGuardedCallback} = require('ReactErrorUtils');
|
||||
var {
|
||||
invokeGuardedCallback,
|
||||
hasCaughtError,
|
||||
clearCaughtError,
|
||||
} = require('ReactErrorUtils');
|
||||
|
||||
var {
|
||||
Placement,
|
||||
@@ -70,14 +74,15 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
|
||||
// Capture errors so they don't interrupt unmounting.
|
||||
function safelyCallComponentWillUnmount(current, instance) {
|
||||
if (__DEV__) {
|
||||
const unmountError = invokeGuardedCallback(
|
||||
invokeGuardedCallback(
|
||||
null,
|
||||
callComponentWillUnmountWithTimerInDev,
|
||||
null,
|
||||
current,
|
||||
instance,
|
||||
);
|
||||
if (unmountError) {
|
||||
if (hasCaughtError()) {
|
||||
const unmountError = clearCaughtError();
|
||||
captureError(current, unmountError);
|
||||
}
|
||||
} else {
|
||||
@@ -93,8 +98,9 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
|
||||
const ref = current.ref;
|
||||
if (ref !== null) {
|
||||
if (__DEV__) {
|
||||
const refError = invokeGuardedCallback(null, ref, null, null);
|
||||
if (refError !== null) {
|
||||
invokeGuardedCallback(null, ref, null, null);
|
||||
if (hasCaughtError()) {
|
||||
const refError = clearCaughtError();
|
||||
captureError(current, refError);
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -29,18 +29,38 @@ function logCapturedError(capturedError: CapturedError): void {
|
||||
return;
|
||||
}
|
||||
|
||||
const error = (capturedError.error: any);
|
||||
|
||||
// Duck-typing
|
||||
let message;
|
||||
let name;
|
||||
let stack;
|
||||
|
||||
if (
|
||||
error !== null &&
|
||||
typeof error.message === 'string' &&
|
||||
typeof error.name === 'string' &&
|
||||
typeof error.stack === 'string'
|
||||
) {
|
||||
message = error.message;
|
||||
name = error.name;
|
||||
stack = error.stack;
|
||||
} else {
|
||||
// A non-error was thrown.
|
||||
message = '' + error;
|
||||
name = 'Error';
|
||||
stack = '';
|
||||
}
|
||||
|
||||
if (__DEV__) {
|
||||
const {
|
||||
componentName,
|
||||
componentStack,
|
||||
error,
|
||||
errorBoundaryName,
|
||||
errorBoundaryFound,
|
||||
willRetry,
|
||||
} = capturedError;
|
||||
|
||||
const {message, name, stack} = error;
|
||||
|
||||
const errorSummary = message ? `${name}: ${message}` : name;
|
||||
|
||||
const componentNameMessage = componentName
|
||||
@@ -85,10 +105,7 @@ function logCapturedError(capturedError: CapturedError): void {
|
||||
`The error is located at: ${componentStack}\n\n` +
|
||||
`The error was thrown at: ${formattedCallStack}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!__DEV__) {
|
||||
const {error} = capturedError;
|
||||
} else {
|
||||
console.error(
|
||||
`React caught an error thrown by one of your components.\n\n${error.stack}`,
|
||||
);
|
||||
|
||||
@@ -21,7 +21,7 @@ import type {HydrationContext} from 'ReactFiberHydrationContext';
|
||||
export type CapturedError = {
|
||||
componentName: ?string,
|
||||
componentStack: string,
|
||||
error: Error,
|
||||
error: mixed,
|
||||
errorBoundary: ?Object,
|
||||
errorBoundaryFound: boolean,
|
||||
errorBoundaryName: string | null,
|
||||
@@ -38,7 +38,11 @@ var {
|
||||
getStackAddendumByWorkInProgressFiber,
|
||||
} = require('ReactFiberComponentTreeHook');
|
||||
var {logCapturedError} = require('ReactFiberErrorLogger');
|
||||
var {invokeGuardedCallback} = require('ReactErrorUtils');
|
||||
var {
|
||||
invokeGuardedCallback,
|
||||
hasCaughtError,
|
||||
clearCaughtError,
|
||||
} = require('ReactErrorUtils');
|
||||
|
||||
var ReactFiberBeginWork = require('ReactFiberBeginWork');
|
||||
var ReactFiberCompleteWork = require('ReactFiberCompleteWork');
|
||||
@@ -222,7 +226,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
|
||||
let failedBoundaries: Set<Fiber> | null = null;
|
||||
// Error boundaries that captured an error during the current commit.
|
||||
let commitPhaseBoundaries: Set<Fiber> | null = null;
|
||||
let firstUncaughtError: Error | null = null;
|
||||
let firstUncaughtError: mixed | null = null;
|
||||
let didFatal: boolean = false;
|
||||
|
||||
let isCommitting: boolean = false;
|
||||
@@ -468,17 +472,23 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
|
||||
startCommitHostEffectsTimer();
|
||||
}
|
||||
while (nextEffect !== null) {
|
||||
let error = null;
|
||||
let didError = false;
|
||||
let error;
|
||||
if (__DEV__) {
|
||||
error = invokeGuardedCallback(null, commitAllHostEffects, null);
|
||||
invokeGuardedCallback(null, commitAllHostEffects, null);
|
||||
if (hasCaughtError()) {
|
||||
didError = true;
|
||||
error = clearCaughtError();
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
commitAllHostEffects();
|
||||
} catch (e) {
|
||||
didError = true;
|
||||
error = e;
|
||||
}
|
||||
}
|
||||
if (error !== null) {
|
||||
if (didError) {
|
||||
invariant(
|
||||
nextEffect !== null,
|
||||
'Should have next effect. This error is likely caused by a bug ' +
|
||||
@@ -512,17 +522,23 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
|
||||
startCommitLifeCyclesTimer();
|
||||
}
|
||||
while (nextEffect !== null) {
|
||||
let error = null;
|
||||
let didError = false;
|
||||
let error;
|
||||
if (__DEV__) {
|
||||
error = invokeGuardedCallback(null, commitAllLifeCycles, null);
|
||||
invokeGuardedCallback(null, commitAllLifeCycles, null);
|
||||
if (hasCaughtError()) {
|
||||
didError = true;
|
||||
error = clearCaughtError();
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
commitAllLifeCycles();
|
||||
} catch (e) {
|
||||
didError = true;
|
||||
error = e;
|
||||
}
|
||||
}
|
||||
if (error !== null) {
|
||||
if (didError) {
|
||||
invariant(
|
||||
nextEffect !== null,
|
||||
'Should have next effect. This error is likely caused by a bug ' +
|
||||
@@ -951,26 +967,25 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
|
||||
// reset it at the end.
|
||||
const previousPriorityContext = priorityContext;
|
||||
|
||||
let error;
|
||||
let didError = false;
|
||||
let error = null;
|
||||
if (__DEV__) {
|
||||
error = invokeGuardedCallback(
|
||||
null,
|
||||
workLoop,
|
||||
null,
|
||||
minPriorityLevel,
|
||||
deadline,
|
||||
);
|
||||
invokeGuardedCallback(null, workLoop, null, minPriorityLevel, deadline);
|
||||
if (hasCaughtError()) {
|
||||
didError = true;
|
||||
error = clearCaughtError();
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
workLoop(minPriorityLevel, deadline);
|
||||
error = null;
|
||||
} catch (e) {
|
||||
didError = true;
|
||||
error = e;
|
||||
}
|
||||
}
|
||||
|
||||
// An error was thrown during the render phase.
|
||||
while (error !== null) {
|
||||
while (didError) {
|
||||
if (didFatal) {
|
||||
// This was a fatal error. Don't attempt to recover from it.
|
||||
firstUncaughtError = error;
|
||||
@@ -1000,8 +1015,10 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
|
||||
continue;
|
||||
}
|
||||
|
||||
didError = false;
|
||||
error = null;
|
||||
if (__DEV__) {
|
||||
error = invokeGuardedCallback(
|
||||
invokeGuardedCallback(
|
||||
null,
|
||||
performWorkCatchBlock,
|
||||
null,
|
||||
@@ -1010,6 +1027,11 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
|
||||
minPriorityLevel,
|
||||
deadline,
|
||||
);
|
||||
if (hasCaughtError()) {
|
||||
didError = true;
|
||||
error = clearCaughtError();
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
performWorkCatchBlock(
|
||||
@@ -1020,16 +1042,11 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
|
||||
);
|
||||
error = null;
|
||||
} catch (e) {
|
||||
didError = true;
|
||||
error = e;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (error !== null) {
|
||||
// Another error was thrown during the render phase. Continue the
|
||||
// loop to handle the new error.
|
||||
continue;
|
||||
}
|
||||
|
||||
// We're finished working. Exit the error loop.
|
||||
break;
|
||||
}
|
||||
@@ -1067,7 +1084,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
|
||||
}
|
||||
|
||||
// Returns the boundary that captured the error, or null if the error is ignored
|
||||
function captureError(failedWork: Fiber, error: Error): Fiber | null {
|
||||
function captureError(failedWork: Fiber, error: mixed): Fiber | null {
|
||||
// It is no longer valid because we exited the user code.
|
||||
ReactCurrentOwner.current = null;
|
||||
if (__DEV__) {
|
||||
|
||||
@@ -14,69 +14,15 @@
|
||||
|
||||
const invariant = require('fbjs/lib/invariant');
|
||||
|
||||
let caughtError = null;
|
||||
|
||||
let invokeGuardedCallback = function(name, func, context, a, b, c, d, e, f) {
|
||||
const funcArgs = Array.prototype.slice.call(arguments, 3);
|
||||
try {
|
||||
func.apply(context, funcArgs);
|
||||
} catch (error) {
|
||||
return error;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
if (__DEV__) {
|
||||
/**
|
||||
* To help development we can get better devtools integration by simulating a
|
||||
* real browser event.
|
||||
*/
|
||||
if (
|
||||
typeof window !== 'undefined' &&
|
||||
typeof window.dispatchEvent === 'function' &&
|
||||
typeof document !== 'undefined' &&
|
||||
typeof document.createEvent === 'function'
|
||||
) {
|
||||
const fakeNode = document.createElement('react');
|
||||
let depth = 0;
|
||||
|
||||
invokeGuardedCallback = function(name, func, context, a, b, c, d, e, f) {
|
||||
depth++;
|
||||
const thisDepth = depth;
|
||||
const funcArgs = Array.prototype.slice.call(arguments, 3);
|
||||
const boundFunc = function() {
|
||||
func.apply(context, funcArgs);
|
||||
};
|
||||
let fakeEventError = null;
|
||||
const onFakeEventError = function(event) {
|
||||
// Don't capture nested errors
|
||||
if (depth === thisDepth) {
|
||||
fakeEventError = event.error;
|
||||
}
|
||||
};
|
||||
const evtType = `react-${name ? name : 'invokeguardedcallback'}-${depth}`;
|
||||
window.addEventListener('error', onFakeEventError);
|
||||
fakeNode.addEventListener(evtType, boundFunc, false);
|
||||
const evt = document.createEvent('Event');
|
||||
evt.initEvent(evtType, false, false);
|
||||
fakeNode.dispatchEvent(evt);
|
||||
fakeNode.removeEventListener(evtType, boundFunc, false);
|
||||
window.removeEventListener('error', onFakeEventError);
|
||||
depth--;
|
||||
return fakeEventError;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let rethrowCaughtError = function() {
|
||||
if (caughtError) {
|
||||
const error = caughtError;
|
||||
caughtError = null;
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const ReactErrorUtils = {
|
||||
// Used by Fiber to simulate a try-catch.
|
||||
_caughtError: null,
|
||||
_hasCaughtError: false,
|
||||
|
||||
// Used by event system to capture/rethrow the first error.
|
||||
_rethrowError: null,
|
||||
_hasRethrowError: false,
|
||||
|
||||
injection: {
|
||||
injectErrorUtils(injectedErrorUtils: Object) {
|
||||
invariant(
|
||||
@@ -106,8 +52,8 @@ const ReactErrorUtils = {
|
||||
d: D,
|
||||
e: E,
|
||||
f: F,
|
||||
): Error | null {
|
||||
return invokeGuardedCallback.apply(this, arguments);
|
||||
): void {
|
||||
invokeGuardedCallback.apply(ReactErrorUtils, arguments);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -130,9 +76,13 @@ const ReactErrorUtils = {
|
||||
e: E,
|
||||
f: F,
|
||||
): void {
|
||||
const error = ReactErrorUtils.invokeGuardedCallback.apply(this, arguments);
|
||||
if (error !== null && caughtError === null) {
|
||||
caughtError = error;
|
||||
ReactErrorUtils.invokeGuardedCallback.apply(this, arguments);
|
||||
if (ReactErrorUtils.hasCaughtError()) {
|
||||
const error = ReactErrorUtils.clearCaughtError();
|
||||
if (!ReactErrorUtils._hasRethrowError) {
|
||||
ReactErrorUtils._hasRethrowError = true;
|
||||
ReactErrorUtils._rethrowError = error;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -141,8 +91,151 @@ const ReactErrorUtils = {
|
||||
* we will rethrow to be handled by the top level error handler.
|
||||
*/
|
||||
rethrowCaughtError: function() {
|
||||
return rethrowCaughtError.apply(this, arguments);
|
||||
return rethrowCaughtError.apply(ReactErrorUtils, arguments);
|
||||
},
|
||||
|
||||
hasCaughtError: function() {
|
||||
return ReactErrorUtils._hasCaughtError;
|
||||
},
|
||||
|
||||
clearCaughtError: function() {
|
||||
if (ReactErrorUtils._hasCaughtError) {
|
||||
const error = ReactErrorUtils._caughtError;
|
||||
ReactErrorUtils._caughtError = null;
|
||||
ReactErrorUtils._hasCaughtError = false;
|
||||
return error;
|
||||
} else {
|
||||
invariant(
|
||||
false,
|
||||
'clearCaughtError was called but no error was captured. This error ' +
|
||||
'is likely caused by a bug in React. Please file an issue.',
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
let invokeGuardedCallback = function(name, func, context, a, b, c, d, e, f) {
|
||||
ReactErrorUtils._hasCaughtError = false;
|
||||
ReactErrorUtils._caughtError = null;
|
||||
const funcArgs = Array.prototype.slice.call(arguments, 3);
|
||||
try {
|
||||
func.apply(context, funcArgs);
|
||||
} catch (error) {
|
||||
ReactErrorUtils._caughtError = error;
|
||||
ReactErrorUtils._hasCaughtError = true;
|
||||
}
|
||||
};
|
||||
|
||||
if (__DEV__) {
|
||||
const ReactFeatureFlags = require('ReactFeatureFlags');
|
||||
|
||||
if (
|
||||
typeof window !== 'undefined' &&
|
||||
typeof window.dispatchEvent === 'function' &&
|
||||
typeof document !== 'undefined' &&
|
||||
typeof document.createEvent === 'function'
|
||||
) {
|
||||
let preventDefault = true;
|
||||
|
||||
/**
|
||||
* To help development we can get better devtools integration by simulating a
|
||||
* real browser event.
|
||||
*/
|
||||
const fakeNode = document.createElement('react');
|
||||
let depth = 0;
|
||||
|
||||
const invokeGuardedCallbackDev = function(
|
||||
name,
|
||||
func,
|
||||
context,
|
||||
a,
|
||||
b,
|
||||
c,
|
||||
d,
|
||||
e,
|
||||
f,
|
||||
) {
|
||||
ReactErrorUtils._hasCaughtError = false;
|
||||
ReactErrorUtils._caughtError = null;
|
||||
|
||||
depth++;
|
||||
const thisDepth = depth;
|
||||
const funcArgs = Array.prototype.slice.call(arguments, 3);
|
||||
const boundFunc = function() {
|
||||
func.apply(context, funcArgs);
|
||||
};
|
||||
const onFakeEventError = function(event) {
|
||||
// Don't capture nested errors
|
||||
if (depth === thisDepth) {
|
||||
ReactErrorUtils._caughtError = event.error;
|
||||
ReactErrorUtils._hasCaughtError = true;
|
||||
}
|
||||
if (preventDefault) {
|
||||
event.preventDefault();
|
||||
}
|
||||
};
|
||||
const evtType = `react-${name ? name : 'invokeguardedcallback'}-${depth}`;
|
||||
window.addEventListener('error', onFakeEventError);
|
||||
fakeNode.addEventListener(evtType, boundFunc, false);
|
||||
const evt = document.createEvent('Event');
|
||||
evt.initEvent(evtType, false, false);
|
||||
fakeNode.dispatchEvent(evt);
|
||||
fakeNode.removeEventListener(evtType, boundFunc, false);
|
||||
window.removeEventListener('error', onFakeEventError);
|
||||
depth--;
|
||||
};
|
||||
|
||||
// Feature test the development version of invokeGuardedCallback
|
||||
// before enabling.
|
||||
let useInvokeGuardedCallbackDev;
|
||||
if (ReactFeatureFlags.forceInvokeGuardedCallbackDev) {
|
||||
// jsdom doesn't handle throwing null correctly (it fails when attempting
|
||||
// to access the 'message' property) but we need the ability to test it.
|
||||
// We use a feature flag to override the default feature test.
|
||||
useInvokeGuardedCallbackDev = true;
|
||||
} else {
|
||||
try {
|
||||
const err = new Error('test');
|
||||
invokeGuardedCallbackDev(
|
||||
null,
|
||||
() => {
|
||||
throw err;
|
||||
},
|
||||
null,
|
||||
);
|
||||
const A = ReactErrorUtils.clearCaughtError();
|
||||
|
||||
invokeGuardedCallbackDev(
|
||||
null,
|
||||
() => {
|
||||
throw null;
|
||||
},
|
||||
null,
|
||||
);
|
||||
const B = ReactErrorUtils.clearCaughtError();
|
||||
|
||||
if (A === err && B === null) {
|
||||
useInvokeGuardedCallbackDev = true;
|
||||
}
|
||||
} catch (e) {
|
||||
useInvokeGuardedCallbackDev = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (useInvokeGuardedCallbackDev) {
|
||||
invokeGuardedCallback = invokeGuardedCallbackDev;
|
||||
preventDefault = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let rethrowCaughtError = function() {
|
||||
if (ReactErrorUtils._hasRethrowError) {
|
||||
const error = ReactErrorUtils._rethrowError;
|
||||
ReactErrorUtils._rethrowError = null;
|
||||
ReactErrorUtils._hasRethrowError = false;
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = ReactErrorUtils;
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
var ReactFeatureFlags = {
|
||||
disableNewFiberFeatures: false,
|
||||
enableAsyncSubtreeAPI: false,
|
||||
// We set this to true when running unit tests
|
||||
forceInvokeGuardedCallbackDev: false,
|
||||
};
|
||||
|
||||
module.exports = ReactFeatureFlags;
|
||||
|
||||
@@ -45,7 +45,7 @@ describe('ReactErrorUtils', () => {
|
||||
});
|
||||
|
||||
function invokeGuardedCallbackTests(environment) {
|
||||
it(`it should rethrow errors caught by invokeGuardedCallbackAndCatchFirstError (${environment})`, () => {
|
||||
it(`it should rethrow caught errors (${environment})`, () => {
|
||||
var err = new Error('foo');
|
||||
var callback = function() {
|
||||
throw err;
|
||||
@@ -55,6 +55,7 @@ describe('ReactErrorUtils', () => {
|
||||
callback,
|
||||
null,
|
||||
);
|
||||
expect(ReactErrorUtils.hasCaughtError()).toBe(false);
|
||||
expect(() => ReactErrorUtils.rethrowCaughtError()).toThrow(err);
|
||||
});
|
||||
|
||||
@@ -82,7 +83,7 @@ describe('ReactErrorUtils', () => {
|
||||
expect(context.didCall).toBe(true);
|
||||
});
|
||||
|
||||
it(`should return a caught error (${environment})`, () => {
|
||||
it(`should catch errors (${environment})`, () => {
|
||||
const error = new Error();
|
||||
const returnValue = ReactErrorUtils.invokeGuardedCallback(
|
||||
'foo',
|
||||
@@ -93,37 +94,39 @@ describe('ReactErrorUtils', () => {
|
||||
'arg1',
|
||||
'arg2',
|
||||
);
|
||||
expect(returnValue).toBe(error);
|
||||
expect(returnValue).toBe(undefined);
|
||||
expect(ReactErrorUtils.hasCaughtError()).toBe(true);
|
||||
expect(ReactErrorUtils.clearCaughtError()).toBe(error);
|
||||
});
|
||||
|
||||
it(`should return null if no error is thrown (${environment})`, () => {
|
||||
it(`should return false from clearCaughtError if no error was thrown (${environment})`, () => {
|
||||
var callback = jest.fn();
|
||||
const returnValue = ReactErrorUtils.invokeGuardedCallback(
|
||||
'foo',
|
||||
callback,
|
||||
null,
|
||||
);
|
||||
expect(returnValue).toBe(null);
|
||||
ReactErrorUtils.invokeGuardedCallback('foo', callback, null);
|
||||
expect(ReactErrorUtils.hasCaughtError()).toBe(false);
|
||||
expect(ReactErrorUtils.clearCaughtError).toThrow('no error');
|
||||
});
|
||||
|
||||
it(`can nest with same debug name (${environment})`, () => {
|
||||
const err1 = new Error();
|
||||
let err2;
|
||||
const err3 = new Error();
|
||||
const err4 = ReactErrorUtils.invokeGuardedCallback(
|
||||
let err4;
|
||||
ReactErrorUtils.invokeGuardedCallback(
|
||||
'foo',
|
||||
function() {
|
||||
err2 = ReactErrorUtils.invokeGuardedCallback(
|
||||
ReactErrorUtils.invokeGuardedCallback(
|
||||
'foo',
|
||||
function() {
|
||||
throw err1;
|
||||
},
|
||||
null,
|
||||
);
|
||||
err2 = ReactErrorUtils.clearCaughtError();
|
||||
throw err3;
|
||||
},
|
||||
null,
|
||||
);
|
||||
err4 = ReactErrorUtils.clearCaughtError();
|
||||
|
||||
expect(err2).toBe(err1);
|
||||
expect(err4).toBe(err3);
|
||||
@@ -132,36 +135,56 @@ describe('ReactErrorUtils', () => {
|
||||
it(`does not return nested errors (${environment})`, () => {
|
||||
const err1 = new Error();
|
||||
let err2;
|
||||
const err3 = ReactErrorUtils.invokeGuardedCallback(
|
||||
ReactErrorUtils.invokeGuardedCallback(
|
||||
'foo',
|
||||
function() {
|
||||
err2 = ReactErrorUtils.invokeGuardedCallback(
|
||||
ReactErrorUtils.invokeGuardedCallback(
|
||||
'foo',
|
||||
function() {
|
||||
throw err1;
|
||||
},
|
||||
null,
|
||||
);
|
||||
err2 = ReactErrorUtils.clearCaughtError();
|
||||
},
|
||||
null,
|
||||
);
|
||||
// Returns null because inner error was already captured
|
||||
expect(ReactErrorUtils.hasCaughtError()).toBe(false);
|
||||
|
||||
expect(err3).toBe(null); // Returns null because inner error was already captured
|
||||
expect(err2).toBe(err1);
|
||||
});
|
||||
|
||||
if (environment === 'production') {
|
||||
// jsdom doesn't handle this properly, but Chrome and Firefox should. Test
|
||||
// this with a fixture.
|
||||
it('catches null values', () => {
|
||||
ReactErrorUtils.invokeGuardedCallback(
|
||||
null,
|
||||
function() {
|
||||
throw null;
|
||||
},
|
||||
null,
|
||||
);
|
||||
expect(ReactErrorUtils.hasCaughtError()).toBe(true);
|
||||
expect(ReactErrorUtils.clearCaughtError()).toBe(null);
|
||||
});
|
||||
}
|
||||
|
||||
it(`can be shimmed (${environment})`, () => {
|
||||
const ops = [];
|
||||
// Override the original invokeGuardedCallback
|
||||
ReactErrorUtils.invokeGuardedCallback = function(name, func, context, a) {
|
||||
ops.push(a);
|
||||
try {
|
||||
func.call(context, a);
|
||||
} catch (error) {
|
||||
return error;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
ReactErrorUtils.injection.injectErrorUtils({
|
||||
invokeGuardedCallback(name, func, context, a) {
|
||||
ops.push(a);
|
||||
try {
|
||||
func.call(context, a);
|
||||
} catch (error) {
|
||||
this._hasCaughtError = true;
|
||||
this._caughtError = error;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
var err = new Error('foo');
|
||||
var callback = function() {
|
||||
@@ -174,9 +197,6 @@ describe('ReactErrorUtils', () => {
|
||||
'somearg',
|
||||
);
|
||||
expect(() => ReactErrorUtils.rethrowCaughtError()).toThrow(err);
|
||||
// invokeGuardedCallbackAndCatchFirstError and rethrowCaughtError close
|
||||
// over ReactErrorUtils.invokeGuardedCallback so should use the
|
||||
// shimmed version.
|
||||
expect(ops).toEqual(['somearg']);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user