@samitouri / QOS-React / commits / 4fcc25a229

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.

Andrew Clark committed Jul 21, 2017 at 15:34 UTC 4fcc25a22972ea07fbb2ddda62f8ccc89c81f175
11 files changed +389 -140
fixtures/dom/src/components/Header.js
+1
@@ -59,6 +59,7 @@ class Header extends React.Component {
59 Input change events
60 </option>
61 <option value="/buttons">Buttons</option>
62 + <option value="/error-handling">Error Handling</option>
63 </select>
64 </label>
65 <label htmlFor="react_version">
fixtures/dom/src/components/fixtures/error-handling/index.js new
+88
@@ -0,0 +1,88 @@
1 +const React = window.React;
2 +
3 +import FixtureSet from '../../FixtureSet';
4 +import TestCase from '../../TestCase';
5 +
6 +function BadRender(props) {
7 + throw props.error;
8 +}
9 +class ErrorBoundary extends React.Component {
10 + state = {
11 + shouldThrow: false,
12 + didThrow: false,
13 + error: null,
14 + };
15 + componentDidCatch(error) {
16 + this.setState({error, didThrow: true});
17 + }
18 + triggerError = () => {
19 + this.setState({
20 + shouldThrow: true,
21 + });
22 + };
23 + render() {
24 + if (this.state.didThrow) {
25 + if (this.state.error) {
26 + return `Captured an error: ${this.state.error.message}`;
27 + } else {
28 + return `Captured an error: ${this.state.error}`;
29 + }
30 + }
31 + if (this.state.shouldThrow) {
32 + return <BadRender error={this.props.error} />;
33 + }
34 + return <button onClick={this.triggerError}>Trigger error</button>;
35 + }
36 +}
37 +class Example extends React.Component {
38 + state = {key: 0};
39 + restart = () => {
40 + this.setState(state => ({key: state.key + 1}));
41 + };
42 + render() {
43 + return (
44 + <div>
45 + <ErrorBoundary error={this.props.error} key={this.state.key} />
46 + <button onClick={this.restart}>Reset</button>
47 + </div>
48 + );
49 + }
50 +}
51 +
52 +export default class ErrorHandlingTestCases extends React.Component {
53 + render() {
54 + return (
55 + <FixtureSet title="Error handling" description="">
56 + <TestCase
57 + title="Break on uncaught exceptions"
58 + description="In DEV, errors should be treated as uncaught, even though React catches them internally">
59 + <TestCase.Steps>
60 + <li>Open the browser DevTools</li>
61 + <li>Make sure "Pause on exceptions" is enabled</li>
62 + <li>Make sure "Pause on caught exceptions" is disabled</li>
63 + <li>Click the "Trigger error" button</li>
64 + <li>Click the reset button</li>
65 + </TestCase.Steps>
66 + <TestCase.ExpectedResult>
67 + The DevTools should pause at the line where the error was thrown, in
68 + the BadRender component. After resuming, the "Trigger error" button
69 + should be replaced with "Captured an error: Oops!" Clicking reset
70 + should reset the test case.
71 + </TestCase.ExpectedResult>
72 + <Example error={new Error('Oops!')} />
73 + </TestCase>
74 + <TestCase title="Throwing null" description="">
75 + <TestCase.Steps>
76 + <li>Click the "Trigger error" button</li>
77 + <li>Click the reset button</li>
78 + </TestCase.Steps>
79 + <TestCase.ExpectedResult>
80 + The "Trigger error" button should be replaced with "Captured an
81 + error: null". Clicking reset should reset the test case.
82 + </TestCase.ExpectedResult>
83 + <Example error={null} />
84 + </TestCase>
85 + </FixtureSet>
86 + );
87 + }
88 +}
fixtures/dom/src/components/fixtures/index.js
+3
@@ -7,6 +7,7 @@ import InputChangeEvents from './input-change-events';
7 import NumberInputFixtures from './number-inputs';
8 import PasswordInputFixtures from './password-inputs';
9 import ButtonFixtures from './buttons';
10 +import ErrorHandling from './error-handling';
11
12 /**
13 * A simple routing component that renders the appropriate
@@ -30,6 +31,8 @@ function FixturesPage() {
31 return <PasswordInputFixtures />;
32 case '/buttons':
33 return <ButtonFixtures />;
34 + case '/error-handling':
35 + return <ErrorHandling />;
36 default:
37 return <p>Please select a test fixture.</p>;
38 }
scripts/fiber/tests-passing.txt
+7 -6
@@ -2328,21 +2328,22 @@ src/renderers/shared/stack/reconciler/__tests__/Transaction-test.js
2328 * should allow nesting of transactions
2329
2330 src/renderers/shared/utils/__tests__/ReactErrorUtils-test.js
2331 -* it should rethrow errors caught by invokeGuardedCallbackAndCatchFirstError (development)
2331 +* it should rethrow caught errors (development)
2332 * should call the callback the passed arguments (development)
2333 * should call the callback with the provided context (development)
2334 -* should return a caught error (development)
2335 -* should return null if no error is thrown (development)
2334 +* should catch errors (development)
2335 +* should return false from clearCaughtError if no error was thrown (development)
2336 * can nest with same debug name (development)
2337 * does not return nested errors (development)
2338 * can be shimmed (development)
2339 -* it should rethrow errors caught by invokeGuardedCallbackAndCatchFirstError (production)
2339 +* it should rethrow caught errors (production)
2340 * should call the callback the passed arguments (production)
2341 * should call the callback with the provided context (production)
2342 -* should return a caught error (production)
2343 -* should return null if no error is thrown (production)
2342 +* should catch errors (production)
2343 +* should return false from clearCaughtError if no error was thrown (production)
2344 * can nest with same debug name (production)
2345 * does not return nested errors (production)
2346 +* catches null values
2347 * can be shimmed (production)
2348
2349 src/renderers/shared/utils/__tests__/accumulateInto-test.js
scripts/jest/fiber.setup.js
+1
@@ -12,6 +12,7 @@ jest.mock('ReactFeatureFlags', () => {
12 const flags = require.requireActual('ReactFeatureFlags');
13 return Object.assign({}, flags, {
14 disableNewFiberFeatures: true,
15 + forceInvokeGuardedCallbackDev: true,
16 });
17 });
18 jest.mock('ReactNativeFeatureFlags', () => {
src/renderers/shared/fiber/ReactFiberCommitWork.js
+11 -5
@@ -26,7 +26,11 @@ var {
26 } = ReactTypeOfWork;
27 var {commitCallbacks} = require('ReactFiberUpdateQueue');
28 var {onCommitUnmount} = require('ReactFiberDevToolsHook');
29 -var {invokeGuardedCallback} = require('ReactErrorUtils');
29 +var {
30 + invokeGuardedCallback,
31 + hasCaughtError,
32 + clearCaughtError,
33 +} = require('ReactErrorUtils');
34
35 var {
36 Placement,
@@ -70,14 +74,15 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
74 // Capture errors so they don't interrupt unmounting.
75 function safelyCallComponentWillUnmount(current, instance) {
76 if (__DEV__) {
73 - const unmountError = invokeGuardedCallback(
77 + invokeGuardedCallback(
78 null,
79 callComponentWillUnmountWithTimerInDev,
80 null,
81 current,
82 instance,
83 );
80 - if (unmountError) {
84 + if (hasCaughtError()) {
85 + const unmountError = clearCaughtError();
86 captureError(current, unmountError);
87 }
88 } else {
@@ -93,8 +98,9 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
98 const ref = current.ref;
99 if (ref !== null) {
100 if (__DEV__) {
96 - const refError = invokeGuardedCallback(null, ref, null, null);
97 - if (refError !== null) {
101 + invokeGuardedCallback(null, ref, null, null);
102 + if (hasCaughtError()) {
103 + const refError = clearCaughtError();
104 captureError(current, refError);
105 }
106 } else {
src/renderers/shared/fiber/ReactFiberErrorLogger.js
+24 -7
@@ -29,18 +29,38 @@ function logCapturedError(capturedError: CapturedError): void {
29 return;
30 }
31
32 + const error = (capturedError.error: any);
33 +
34 + // Duck-typing
35 + let message;
36 + let name;
37 + let stack;
38 +
39 + if (
40 + error !== null &&
41 + typeof error.message === 'string' &&
42 + typeof error.name === 'string' &&
43 + typeof error.stack === 'string'
44 + ) {
45 + message = error.message;
46 + name = error.name;
47 + stack = error.stack;
48 + } else {
49 + // A non-error was thrown.
50 + message = '' + error;
51 + name = 'Error';
52 + stack = '';
53 + }
54 +
55 if (__DEV__) {
56 const {
57 componentName,
58 componentStack,
36 - error,
59 errorBoundaryName,
60 errorBoundaryFound,
61 willRetry,
62 } = capturedError;
63
42 - const {message, name, stack} = error;
43 -
64 const errorSummary = message ? `${name}: ${message}` : name;
65
66 const componentNameMessage = componentName
@@ -85,10 +105,7 @@ function logCapturedError(capturedError: CapturedError): void {
105 `The error is located at: ${componentStack}\n\n` +
106 `The error was thrown at: ${formattedCallStack}`,
107 );
88 - }
89 -
90 - if (!__DEV__) {
91 - const {error} = capturedError;
108 + } else {
109 console.error(
110 `React caught an error thrown by one of your components.\n\n${error.stack}`,
111 );
src/renderers/shared/fiber/ReactFiberScheduler.js
+45 -28
@@ -21,7 +21,7 @@ import type {HydrationContext} from 'ReactFiberHydrationContext';
21 export type CapturedError = {
22 componentName: ?string,
23 componentStack: string,
24 - error: Error,
24 + error: mixed,
25 errorBoundary: ?Object,
26 errorBoundaryFound: boolean,
27 errorBoundaryName: string | null,
@@ -38,7 +38,11 @@ var {
38 getStackAddendumByWorkInProgressFiber,
39 } = require('ReactFiberComponentTreeHook');
40 var {logCapturedError} = require('ReactFiberErrorLogger');
41 -var {invokeGuardedCallback} = require('ReactErrorUtils');
41 +var {
42 + invokeGuardedCallback,
43 + hasCaughtError,
44 + clearCaughtError,
45 +} = require('ReactErrorUtils');
46
47 var ReactFiberBeginWork = require('ReactFiberBeginWork');
48 var ReactFiberCompleteWork = require('ReactFiberCompleteWork');
@@ -222,7 +226,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
226 let failedBoundaries: Set<Fiber> | null = null;
227 // Error boundaries that captured an error during the current commit.
228 let commitPhaseBoundaries: Set<Fiber> | null = null;
225 - let firstUncaughtError: Error | null = null;
229 + let firstUncaughtError: mixed | null = null;
230 let didFatal: boolean = false;
231
232 let isCommitting: boolean = false;
@@ -468,17 +472,23 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
472 startCommitHostEffectsTimer();
473 }
474 while (nextEffect !== null) {
471 - let error = null;
475 + let didError = false;
476 + let error;
477 if (__DEV__) {
473 - error = invokeGuardedCallback(null, commitAllHostEffects, null);
478 + invokeGuardedCallback(null, commitAllHostEffects, null);
479 + if (hasCaughtError()) {
480 + didError = true;
481 + error = clearCaughtError();
482 + }
483 } else {
484 try {
485 commitAllHostEffects();
486 } catch (e) {
487 + didError = true;
488 error = e;
489 }
490 }
481 - if (error !== null) {
491 + if (didError) {
492 invariant(
493 nextEffect !== null,
494 '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>(
522 startCommitLifeCyclesTimer();
523 }
524 while (nextEffect !== null) {
515 - let error = null;
525 + let didError = false;
526 + let error;
527 if (__DEV__) {
517 - error = invokeGuardedCallback(null, commitAllLifeCycles, null);
528 + invokeGuardedCallback(null, commitAllLifeCycles, null);
529 + if (hasCaughtError()) {
530 + didError = true;
531 + error = clearCaughtError();
532 + }
533 } else {
534 try {
535 commitAllLifeCycles();
536 } catch (e) {
537 + didError = true;
538 error = e;
539 }
540 }
525 - if (error !== null) {
541 + if (didError) {
542 invariant(
543 nextEffect !== null,
544 '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>(
967 // reset it at the end.
968 const previousPriorityContext = priorityContext;
969
954 - let error;
970 + let didError = false;
971 + let error = null;
972 if (__DEV__) {
956 - error = invokeGuardedCallback(
957 - null,
958 - workLoop,
959 - null,
960 - minPriorityLevel,
961 - deadline,
962 - );
973 + invokeGuardedCallback(null, workLoop, null, minPriorityLevel, deadline);
974 + if (hasCaughtError()) {
975 + didError = true;
976 + error = clearCaughtError();
977 + }
978 } else {
979 try {
980 workLoop(minPriorityLevel, deadline);
966 - error = null;
981 } catch (e) {
982 + didError = true;
983 error = e;
984 }
985 }
986
987 // An error was thrown during the render phase.
973 - while (error !== null) {
988 + while (didError) {
989 if (didFatal) {
990 // This was a fatal error. Don't attempt to recover from it.
991 firstUncaughtError = error;
@@ -1000,8 +1015,10 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
1015 continue;
1016 }
1017
1018 + didError = false;
1019 + error = null;
1020 if (__DEV__) {
1004 - error = invokeGuardedCallback(
1021 + invokeGuardedCallback(
1022 null,
1023 performWorkCatchBlock,
1024 null,
@@ -1010,6 +1027,11 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
1027 minPriorityLevel,
1028 deadline,
1029 );
1030 + if (hasCaughtError()) {
1031 + didError = true;
1032 + error = clearCaughtError();
1033 + continue;
1034 + }
1035 } else {
1036 try {
1037 performWorkCatchBlock(
@@ -1020,16 +1042,11 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
1042 );
1043 error = null;
1044 } catch (e) {
1045 + didError = true;
1046 error = e;
1047 + continue;
1048 }
1049 }
1026 -
1027 - if (error !== null) {
1028 - // Another error was thrown during the render phase. Continue the
1029 - // loop to handle the new error.
1030 - continue;
1031 - }
1032 -
1050 // We're finished working. Exit the error loop.
1051 break;
1052 }
@@ -1067,7 +1084,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
1084 }
1085
1086 // Returns the boundary that captured the error, or null if the error is ignored
1070 - function captureError(failedWork: Fiber, error: Error): Fiber | null {
1087 + function captureError(failedWork: Fiber, error: mixed): Fiber | null {
1088 // It is no longer valid because we exited the user code.
1089 ReactCurrentOwner.current = null;
1090 if (__DEV__) {
src/renderers/shared/utils/ReactErrorUtils.js
+160 -67
@@ -14,69 +14,15 @@
14
15 const invariant = require('fbjs/lib/invariant');
16
17 -let caughtError = null;
18 -
19 -let invokeGuardedCallback = function(name, func, context, a, b, c, d, e, f) {
20 - const funcArgs = Array.prototype.slice.call(arguments, 3);
21 - try {
22 - func.apply(context, funcArgs);
23 - } catch (error) {
24 - return error;
25 - }
26 - return null;
27 -};
28 -
29 -if (__DEV__) {
30 - /**
31 - * To help development we can get better devtools integration by simulating a
32 - * real browser event.
33 - */
34 - if (
35 - typeof window !== 'undefined' &&
36 - typeof window.dispatchEvent === 'function' &&
37 - typeof document !== 'undefined' &&
38 - typeof document.createEvent === 'function'
39 - ) {
40 - const fakeNode = document.createElement('react');
41 - let depth = 0;
42 -
43 - invokeGuardedCallback = function(name, func, context, a, b, c, d, e, f) {
44 - depth++;
45 - const thisDepth = depth;
46 - const funcArgs = Array.prototype.slice.call(arguments, 3);
47 - const boundFunc = function() {
48 - func.apply(context, funcArgs);
49 - };
50 - let fakeEventError = null;
51 - const onFakeEventError = function(event) {
52 - // Don't capture nested errors
53 - if (depth === thisDepth) {
54 - fakeEventError = event.error;
55 - }
56 - };
57 - const evtType = `react-${name ? name : 'invokeguardedcallback'}-${depth}`;
58 - window.addEventListener('error', onFakeEventError);
59 - fakeNode.addEventListener(evtType, boundFunc, false);
60 - const evt = document.createEvent('Event');
61 - evt.initEvent(evtType, false, false);
62 - fakeNode.dispatchEvent(evt);
63 - fakeNode.removeEventListener(evtType, boundFunc, false);
64 - window.removeEventListener('error', onFakeEventError);
65 - depth--;
66 - return fakeEventError;
67 - };
68 - }
69 -}
17 +const ReactErrorUtils = {
18 + // Used by Fiber to simulate a try-catch.
19 + _caughtError: null,
20 + _hasCaughtError: false,
21
71 -let rethrowCaughtError = function() {
72 - if (caughtError) {
73 - const error = caughtError;
74 - caughtError = null;
75 - throw error;
76 - }
77 -};
22 + // Used by event system to capture/rethrow the first error.
23 + _rethrowError: null,
24 + _hasRethrowError: false,
25
79 -const ReactErrorUtils = {
26 injection: {
27 injectErrorUtils(injectedErrorUtils: Object) {
28 invariant(
@@ -106,8 +52,8 @@ const ReactErrorUtils = {
52 d: D,
53 e: E,
54 f: F,
109 - ): Error | null {
110 - return invokeGuardedCallback.apply(this, arguments);
55 + ): void {
56 + invokeGuardedCallback.apply(ReactErrorUtils, arguments);
57 },
58
59 /**
@@ -130,9 +76,13 @@ const ReactErrorUtils = {
76 e: E,
77 f: F,
78 ): void {
133 - const error = ReactErrorUtils.invokeGuardedCallback.apply(this, arguments);
134 - if (error !== null && caughtError === null) {
135 - caughtError = error;
79 + ReactErrorUtils.invokeGuardedCallback.apply(this, arguments);
80 + if (ReactErrorUtils.hasCaughtError()) {
81 + const error = ReactErrorUtils.clearCaughtError();
82 + if (!ReactErrorUtils._hasRethrowError) {
83 + ReactErrorUtils._hasRethrowError = true;
84 + ReactErrorUtils._rethrowError = error;
85 + }
86 }
87 },
88
@@ -141,8 +91,151 @@ const ReactErrorUtils = {
91 * we will rethrow to be handled by the top level error handler.
92 */
93 rethrowCaughtError: function() {
144 - return rethrowCaughtError.apply(this, arguments);
94 + return rethrowCaughtError.apply(ReactErrorUtils, arguments);
95 + },
96 +
97 + hasCaughtError: function() {
98 + return ReactErrorUtils._hasCaughtError;
99 + },
100 +
101 + clearCaughtError: function() {
102 + if (ReactErrorUtils._hasCaughtError) {
103 + const error = ReactErrorUtils._caughtError;
104 + ReactErrorUtils._caughtError = null;
105 + ReactErrorUtils._hasCaughtError = false;
106 + return error;
107 + } else {
108 + invariant(
109 + false,
110 + 'clearCaughtError was called but no error was captured. This error ' +
111 + 'is likely caused by a bug in React. Please file an issue.',
112 + );
113 + }
114 },
115 };
116
117 +let invokeGuardedCallback = function(name, func, context, a, b, c, d, e, f) {
118 + ReactErrorUtils._hasCaughtError = false;
119 + ReactErrorUtils._caughtError = null;
120 + const funcArgs = Array.prototype.slice.call(arguments, 3);
121 + try {
122 + func.apply(context, funcArgs);
123 + } catch (error) {
124 + ReactErrorUtils._caughtError = error;
125 + ReactErrorUtils._hasCaughtError = true;
126 + }
127 +};
128 +
129 +if (__DEV__) {
130 + const ReactFeatureFlags = require('ReactFeatureFlags');
131 +
132 + if (
133 + typeof window !== 'undefined' &&
134 + typeof window.dispatchEvent === 'function' &&
135 + typeof document !== 'undefined' &&
136 + typeof document.createEvent === 'function'
137 + ) {
138 + let preventDefault = true;
139 +
140 + /**
141 + * To help development we can get better devtools integration by simulating a
142 + * real browser event.
143 + */
144 + const fakeNode = document.createElement('react');
145 + let depth = 0;
146 +
147 + const invokeGuardedCallbackDev = function(
148 + name,
149 + func,
150 + context,
151 + a,
152 + b,
153 + c,
154 + d,
155 + e,
156 + f,
157 + ) {
158 + ReactErrorUtils._hasCaughtError = false;
159 + ReactErrorUtils._caughtError = null;
160 +
161 + depth++;
162 + const thisDepth = depth;
163 + const funcArgs = Array.prototype.slice.call(arguments, 3);
164 + const boundFunc = function() {
165 + func.apply(context, funcArgs);
166 + };
167 + const onFakeEventError = function(event) {
168 + // Don't capture nested errors
169 + if (depth === thisDepth) {
170 + ReactErrorUtils._caughtError = event.error;
171 + ReactErrorUtils._hasCaughtError = true;
172 + }
173 + if (preventDefault) {
174 + event.preventDefault();
175 + }
176 + };
177 + const evtType = `react-${name ? name : 'invokeguardedcallback'}-${depth}`;
178 + window.addEventListener('error', onFakeEventError);
179 + fakeNode.addEventListener(evtType, boundFunc, false);
180 + const evt = document.createEvent('Event');
181 + evt.initEvent(evtType, false, false);
182 + fakeNode.dispatchEvent(evt);
183 + fakeNode.removeEventListener(evtType, boundFunc, false);
184 + window.removeEventListener('error', onFakeEventError);
185 + depth--;
186 + };
187 +
188 + // Feature test the development version of invokeGuardedCallback
189 + // before enabling.
190 + let useInvokeGuardedCallbackDev;
191 + if (ReactFeatureFlags.forceInvokeGuardedCallbackDev) {
192 + // jsdom doesn't handle throwing null correctly (it fails when attempting
193 + // to access the 'message' property) but we need the ability to test it.
194 + // We use a feature flag to override the default feature test.
195 + useInvokeGuardedCallbackDev = true;
196 + } else {
197 + try {
198 + const err = new Error('test');
199 + invokeGuardedCallbackDev(
200 + null,
201 + () => {
202 + throw err;
203 + },
204 + null,
205 + );
206 + const A = ReactErrorUtils.clearCaughtError();
207 +
208 + invokeGuardedCallbackDev(
209 + null,
210 + () => {
211 + throw null;
212 + },
213 + null,
214 + );
215 + const B = ReactErrorUtils.clearCaughtError();
216 +
217 + if (A === err && B === null) {
218 + useInvokeGuardedCallbackDev = true;
219 + }
220 + } catch (e) {
221 + useInvokeGuardedCallbackDev = false;
222 + }
223 + }
224 +
225 + if (useInvokeGuardedCallbackDev) {
226 + invokeGuardedCallback = invokeGuardedCallbackDev;
227 + preventDefault = false;
228 + }
229 + }
230 +}
231 +
232 +let rethrowCaughtError = function() {
233 + if (ReactErrorUtils._hasRethrowError) {
234 + const error = ReactErrorUtils._rethrowError;
235 + ReactErrorUtils._rethrowError = null;
236 + ReactErrorUtils._hasRethrowError = false;
237 + throw error;
238 + }
239 +};
240 +
241 module.exports = ReactErrorUtils;
src/renderers/shared/utils/ReactFeatureFlags.js
+2
@@ -15,6 +15,8 @@
15 var ReactFeatureFlags = {
16 disableNewFiberFeatures: false,
17 enableAsyncSubtreeAPI: false,
18 + // We set this to true when running unit tests
19 + forceInvokeGuardedCallbackDev: false,
20 };
21
22 module.exports = ReactFeatureFlags;
src/renderers/shared/utils/__tests__/ReactErrorUtils-test.js
+47 -27
@@ -45,7 +45,7 @@ describe('ReactErrorUtils', () => {
45 });
46
47 function invokeGuardedCallbackTests(environment) {
48 - it(`it should rethrow errors caught by invokeGuardedCallbackAndCatchFirstError (${environment})`, () => {
48 + it(`it should rethrow caught errors (${environment})`, () => {
49 var err = new Error('foo');
50 var callback = function() {
51 throw err;
@@ -55,6 +55,7 @@ describe('ReactErrorUtils', () => {
55 callback,
56 null,
57 );
58 + expect(ReactErrorUtils.hasCaughtError()).toBe(false);
59 expect(() => ReactErrorUtils.rethrowCaughtError()).toThrow(err);
60 });
61
@@ -82,7 +83,7 @@ describe('ReactErrorUtils', () => {
83 expect(context.didCall).toBe(true);
84 });
85
85 - it(`should return a caught error (${environment})`, () => {
86 + it(`should catch errors (${environment})`, () => {
87 const error = new Error();
88 const returnValue = ReactErrorUtils.invokeGuardedCallback(
89 'foo',
@@ -93,37 +94,39 @@ describe('ReactErrorUtils', () => {
94 'arg1',
95 'arg2',
96 );
96 - expect(returnValue).toBe(error);
97 + expect(returnValue).toBe(undefined);
98 + expect(ReactErrorUtils.hasCaughtError()).toBe(true);
99 + expect(ReactErrorUtils.clearCaughtError()).toBe(error);
100 });
101
99 - it(`should return null if no error is thrown (${environment})`, () => {
102 + it(`should return false from clearCaughtError if no error was thrown (${environment})`, () => {
103 var callback = jest.fn();
101 - const returnValue = ReactErrorUtils.invokeGuardedCallback(
102 - 'foo',
103 - callback,
104 - null,
105 - );
106 - expect(returnValue).toBe(null);
104 + ReactErrorUtils.invokeGuardedCallback('foo', callback, null);
105 + expect(ReactErrorUtils.hasCaughtError()).toBe(false);
106 + expect(ReactErrorUtils.clearCaughtError).toThrow('no error');
107 });
108
109 it(`can nest with same debug name (${environment})`, () => {
110 const err1 = new Error();
111 let err2;
112 const err3 = new Error();
113 - const err4 = ReactErrorUtils.invokeGuardedCallback(
113 + let err4;
114 + ReactErrorUtils.invokeGuardedCallback(
115 'foo',
116 function() {
116 - err2 = ReactErrorUtils.invokeGuardedCallback(
117 + ReactErrorUtils.invokeGuardedCallback(
118 'foo',
119 function() {
120 throw err1;
121 },
122 null,
123 );
124 + err2 = ReactErrorUtils.clearCaughtError();
125 throw err3;
126 },
127 null,
128 );
129 + err4 = ReactErrorUtils.clearCaughtError();
130
131 expect(err2).toBe(err1);
132 expect(err4).toBe(err3);
@@ -132,36 +135,56 @@ describe('ReactErrorUtils', () => {
135 it(`does not return nested errors (${environment})`, () => {
136 const err1 = new Error();
137 let err2;
135 - const err3 = ReactErrorUtils.invokeGuardedCallback(
138 + ReactErrorUtils.invokeGuardedCallback(
139 'foo',
140 function() {
138 - err2 = ReactErrorUtils.invokeGuardedCallback(
141 + ReactErrorUtils.invokeGuardedCallback(
142 'foo',
143 function() {
144 throw err1;
145 },
146 null,
147 );
148 + err2 = ReactErrorUtils.clearCaughtError();
149 },
150 null,
151 );
152 + // Returns null because inner error was already captured
153 + expect(ReactErrorUtils.hasCaughtError()).toBe(false);
154
149 - expect(err3).toBe(null); // Returns null because inner error was already captured
155 expect(err2).toBe(err1);
156 });
157
158 + if (environment === 'production') {
159 + // jsdom doesn't handle this properly, but Chrome and Firefox should. Test
160 + // this with a fixture.
161 + it('catches null values', () => {
162 + ReactErrorUtils.invokeGuardedCallback(
163 + null,
164 + function() {
165 + throw null;
166 + },
167 + null,
168 + );
169 + expect(ReactErrorUtils.hasCaughtError()).toBe(true);
170 + expect(ReactErrorUtils.clearCaughtError()).toBe(null);
171 + });
172 + }
173 +
174 it(`can be shimmed (${environment})`, () => {
175 const ops = [];
176 // Override the original invokeGuardedCallback
156 - ReactErrorUtils.invokeGuardedCallback = function(name, func, context, a) {
157 - ops.push(a);
158 - try {
159 - func.call(context, a);
160 - } catch (error) {
161 - return error;
162 - }
163 - return null;
164 - };
177 + ReactErrorUtils.injection.injectErrorUtils({
178 + invokeGuardedCallback(name, func, context, a) {
179 + ops.push(a);
180 + try {
181 + func.call(context, a);
182 + } catch (error) {
183 + this._hasCaughtError = true;
184 + this._caughtError = error;
185 + }
186 + },
187 + });
188
189 var err = new Error('foo');
190 var callback = function() {
@@ -174,9 +197,6 @@ describe('ReactErrorUtils', () => {
197 'somearg',
198 );
199 expect(() => ReactErrorUtils.rethrowCaughtError()).toThrow(err);
177 - // invokeGuardedCallbackAndCatchFirstError and rethrowCaughtError close
178 - // over ReactErrorUtils.invokeGuardedCallback so should use the
179 - // shimmed version.
200 expect(ops).toEqual(['somearg']);
201 });
202 }