diff --git a/packages/react-dom/src/client/ReactDOMFB.js b/packages/react-dom/src/client/ReactDOMFB.js
index 8b846cf4a91..f215da5530e 100644
--- a/packages/react-dom/src/client/ReactDOMFB.js
+++ b/packages/react-dom/src/client/ReactDOMFB.js
@@ -9,7 +9,6 @@
import * as ReactFiberTreeReflection from 'react-reconciler/reflection';
import * as ReactInstanceMap from 'shared/ReactInstanceMap';
-import ReactErrorUtils from 'shared/ReactErrorUtils';
import {addUserTimingListener} from 'shared/ReactFeatureFlags';
import ReactDOM from './ReactDOM';
@@ -22,7 +21,6 @@ Object.assign(
{
// These are real internal dependencies that are trickier to remove:
ReactBrowserEventEmitter,
- ReactErrorUtils,
ReactFiberTreeReflection,
ReactDOMComponentTree,
ReactInstanceMap,
diff --git a/packages/shared/ReactErrorUtils.js b/packages/shared/ReactErrorUtils.js
index cbbfc81652b..5e13a7b9747 100644
--- a/packages/shared/ReactErrorUtils.js
+++ b/packages/shared/ReactErrorUtils.js
@@ -8,6 +8,7 @@
*/
import invariant from 'fbjs/lib/invariant';
+import invokeGuardedCallback from './invokeGuardedCallback';
const ReactErrorUtils = {
// Used by Fiber to simulate a try-catch.
@@ -18,16 +19,6 @@ const ReactErrorUtils = {
_rethrowError: (null: mixed),
_hasRethrowError: (false: boolean),
- injection: {
- injectErrorUtils(injectedErrorUtils: Object) {
- invariant(
- typeof injectedErrorUtils.invokeGuardedCallback === 'function',
- 'Injected invokeGuardedCallback() must be a function.',
- );
- invokeGuardedCallback = injectedErrorUtils.invokeGuardedCallback;
- },
- },
-
/**
* Call a function while guarding against errors that happens within it.
* Returns an error if it throws, otherwise null.
@@ -114,168 +105,6 @@ const ReactErrorUtils = {
},
};
-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__) {
- // In DEV mode, we swap out invokeGuardedCallback for a special version
- // that plays more nicely with the browser's DevTools. The idea is to preserve
- // "Pause on exceptions" behavior. Because React wraps all user-provided
- // functions in invokeGuardedCallback, and the production version of
- // invokeGuardedCallback uses a try-catch, all user exceptions are treated
- // like caught exceptions, and the DevTools won't pause unless the developer
- // takes the extra step of enabling pause on caught exceptions. This is
- // untintuitive, though, because even though React has caught the error, from
- // the developer's perspective, the error is uncaught.
- //
- // To preserve the expected "Pause on exceptions" behavior, we don't use a
- // try-catch in DEV. Instead, we synchronously dispatch a fake event to a fake
- // DOM node, and call the user-provided callback from inside an event handler
- // for that fake event. If the callback throws, the error is "captured" using
- // a global event handler. But because the error happens in a different
- // event loop context, it does not interrupt the normal program flow.
- // Effectively, this gives us try-catch behavior without actually using
- // try-catch. Neat!
-
- // Check that the browser supports the APIs we need to implement our special
- // DEV version of invokeGuardedCallback
- if (
- typeof window !== 'undefined' &&
- typeof window.dispatchEvent === 'function' &&
- typeof document !== 'undefined' &&
- typeof document.createEvent === 'function'
- ) {
- const fakeNode = document.createElement('react');
-
- const invokeGuardedCallbackDev = function(
- name,
- func,
- context,
- a,
- b,
- c,
- d,
- e,
- f,
- ) {
- // If document doesn't exist we know for sure we will crash in this method
- // when we call document.createEvent(). However this can cause confusing
- // errors: https://github.com/facebookincubator/create-react-app/issues/3482
- // So we preemptively throw with a better message instead.
- invariant(
- typeof document !== 'undefined',
- 'The `document` global was defined when React was initialized, but is not ' +
- 'defined anymore. This can happen in a test environment if a component ' +
- 'schedules an update from an asynchronous callback, but the test has already ' +
- 'finished running. To solve this, you can either unmount the component at ' +
- 'the end of your test (and ensure that any asynchronous operations get ' +
- 'canceled in `componentWillUnmount`), or you can change the test itself ' +
- 'to be asynchronous.',
- );
- const evt = document.createEvent('Event');
-
- // Keeps track of whether the user-provided callback threw an error. We
- // set this to true at the beginning, then set it to false right after
- // calling the function. If the function errors, `didError` will never be
- // set to false. This strategy works even if the browser is flaky and
- // fails to call our global error handler, because it doesn't rely on
- // the error event at all.
- let didError = true;
-
- // Create an event handler for our fake event. We will synchronously
- // dispatch our fake event using `dispatchEvent`. Inside the handler, we
- // call the user-provided callback.
- const funcArgs = Array.prototype.slice.call(arguments, 3);
- function callCallback() {
- // We immediately remove the callback from event listeners so that
- // nested `invokeGuardedCallback` calls do not clash. Otherwise, a
- // nested call would trigger the fake event handlers of any call higher
- // in the stack.
- fakeNode.removeEventListener(evtType, callCallback, false);
- func.apply(context, funcArgs);
- didError = false;
- }
-
- // Create a global error event handler. We use this to capture the value
- // that was thrown. It's possible that this error handler will fire more
- // than once; for example, if non-React code also calls `dispatchEvent`
- // and a handler for that event throws. We should be resilient to most of
- // those cases. Even if our error event handler fires more than once, the
- // last error event is always used. If the callback actually does error,
- // we know that the last error event is the correct one, because it's not
- // possible for anything else to have happened in between our callback
- // erroring and the code that follows the `dispatchEvent` call below. If
- // the callback doesn't error, but the error event was fired, we know to
- // ignore it because `didError` will be false, as described above.
- let error;
- // Use this to track whether the error event is ever called.
- let didSetError = false;
- let isCrossOriginError = false;
-
- function onError(event) {
- error = event.error;
- didSetError = true;
- if (error === null && event.colno === 0 && event.lineno === 0) {
- isCrossOriginError = true;
- }
- }
-
- // Create a fake event type.
- const evtType = `react-${name ? name : 'invokeguardedcallback'}`;
-
- // Attach our event handlers
- window.addEventListener('error', onError);
- fakeNode.addEventListener(evtType, callCallback, false);
-
- // Synchronously dispatch our fake event. If the user-provided function
- // errors, it will trigger our global error handler.
- evt.initEvent(evtType, false, false);
- fakeNode.dispatchEvent(evt);
-
- if (didError) {
- if (!didSetError) {
- // The callback errored, but the error event never fired.
- error = new Error(
- 'An error was thrown inside one of your components, but React ' +
- "doesn't know what it was. This is likely due to browser " +
- 'flakiness. React does its best to preserve the "Pause on ' +
- 'exceptions" behavior of the DevTools, which requires some ' +
- "DEV-mode only tricks. It's possible that these don't work in " +
- 'your browser. Try triggering the error in production mode, ' +
- 'or switching to a modern browser. If you suspect that this is ' +
- 'actually an issue with React, please file an issue.',
- );
- } else if (isCrossOriginError) {
- error = new Error(
- "A cross-origin error was thrown. React doesn't have access to " +
- 'the actual error object in development. ' +
- 'See https://fb.me/react-crossorigin-error for more information.',
- );
- }
- ReactErrorUtils._hasCaughtError = true;
- ReactErrorUtils._caughtError = error;
- } else {
- ReactErrorUtils._hasCaughtError = false;
- ReactErrorUtils._caughtError = null;
- }
-
- // Remove our event listeners
- window.removeEventListener('error', onError);
- };
-
- invokeGuardedCallback = invokeGuardedCallbackDev;
- }
-}
-
let rethrowCaughtError = function() {
if (ReactErrorUtils._hasRethrowError) {
const error = ReactErrorUtils._rethrowError;
diff --git a/packages/shared/__tests__/ReactErrorUtils-test.internal.js b/packages/shared/__tests__/ReactErrorUtils-test.internal.js
index 30cb6d824aa..9a064e4312a 100644
--- a/packages/shared/__tests__/ReactErrorUtils-test.internal.js
+++ b/packages/shared/__tests__/ReactErrorUtils-test.internal.js
@@ -176,30 +176,37 @@ describe('ReactErrorUtils', () => {
it(`can be shimmed`, () => {
const ops = [];
- // Override the original invokeGuardedCallback
- 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() {
- throw err;
- };
- ReactErrorUtils.invokeGuardedCallbackAndCatchFirstError(
- 'foo',
- callback,
- null,
- 'somearg',
+ jest.resetModules();
+ jest.mock(
+ 'shared/invokeGuardedCallback',
+ () =>
+ function invokeGuardedCallback(name, func, context, a) {
+ ops.push(a);
+ try {
+ func.call(context, a);
+ } catch (error) {
+ this._hasCaughtError = true;
+ this._caughtError = error;
+ }
+ },
);
- expect(() => ReactErrorUtils.rethrowCaughtError()).toThrow(err);
- expect(ops).toEqual(['somearg']);
+ ReactErrorUtils = require('shared/ReactErrorUtils').default;
+
+ try {
+ var err = new Error('foo');
+ var callback = function() {
+ throw err;
+ };
+ ReactErrorUtils.invokeGuardedCallbackAndCatchFirstError(
+ 'foo',
+ callback,
+ null,
+ 'somearg',
+ );
+ expect(() => ReactErrorUtils.rethrowCaughtError()).toThrow(err);
+ expect(ops).toEqual(['somearg']);
+ } finally {
+ jest.unmock('shared/invokeGuardedCallback');
+ }
});
});
diff --git a/packages/shared/forks/invokeGuardedCallback.www.js b/packages/shared/forks/invokeGuardedCallback.www.js
new file mode 100644
index 00000000000..3cbe0d6536c
--- /dev/null
+++ b/packages/shared/forks/invokeGuardedCallback.www.js
@@ -0,0 +1,17 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import invariant from 'fbjs/lib/invariant';
+
+const invokeGuardedCallback = require('ReactFbErrorUtils')
+ .invokeGuardedCallback;
+invariant(
+ typeof invokeGuardedCallback === 'function',
+ 'Expected ReactFbErrorUtils.invokeGuardedCallback to be a function.',
+);
+
+export default invokeGuardedCallback;
diff --git a/packages/shared/invokeGuardedCallback.js b/packages/shared/invokeGuardedCallback.js
new file mode 100644
index 00000000000..0abe767fdf0
--- /dev/null
+++ b/packages/shared/invokeGuardedCallback.js
@@ -0,0 +1,184 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @flow
+ */
+
+import invariant from 'fbjs/lib/invariant';
+
+let invokeGuardedCallback = function(
+ name: string | null,
+ func: (a: A, b: B, c: C, d: D, e: E, f: F) => void,
+ context: Context,
+ a: A,
+ b: B,
+ c: C,
+ d: D,
+ e: E,
+ f: F,
+) {
+ this._hasCaughtError = false;
+ this._caughtError = null;
+ const funcArgs = Array.prototype.slice.call(arguments, 3);
+ try {
+ func.apply(context, funcArgs);
+ } catch (error) {
+ this._caughtError = error;
+ this._hasCaughtError = true;
+ }
+};
+
+if (__DEV__) {
+ // In DEV mode, we swap out invokeGuardedCallback for a special version
+ // that plays more nicely with the browser's DevTools. The idea is to preserve
+ // "Pause on exceptions" behavior. Because React wraps all user-provided
+ // functions in invokeGuardedCallback, and the production version of
+ // invokeGuardedCallback uses a try-catch, all user exceptions are treated
+ // like caught exceptions, and the DevTools won't pause unless the developer
+ // takes the extra step of enabling pause on caught exceptions. This is
+ // untintuitive, though, because even though React has caught the error, from
+ // the developer's perspective, the error is uncaught.
+ //
+ // To preserve the expected "Pause on exceptions" behavior, we don't use a
+ // try-catch in DEV. Instead, we synchronously dispatch a fake event to a fake
+ // DOM node, and call the user-provided callback from inside an event handler
+ // for that fake event. If the callback throws, the error is "captured" using
+ // a global event handler. But because the error happens in a different
+ // event loop context, it does not interrupt the normal program flow.
+ // Effectively, this gives us try-catch behavior without actually using
+ // try-catch. Neat!
+
+ // Check that the browser supports the APIs we need to implement our special
+ // DEV version of invokeGuardedCallback
+ if (
+ typeof window !== 'undefined' &&
+ typeof window.dispatchEvent === 'function' &&
+ typeof document !== 'undefined' &&
+ typeof document.createEvent === 'function'
+ ) {
+ const fakeNode = document.createElement('react');
+
+ const invokeGuardedCallbackDev = function(
+ name: string | null,
+ func: (a: A, b: B, c: C, d: D, e: E, f: F) => void,
+ context: Context,
+ a: A,
+ b: B,
+ c: C,
+ d: D,
+ e: E,
+ f: F,
+ ) {
+ // If document doesn't exist we know for sure we will crash in this method
+ // when we call document.createEvent(). However this can cause confusing
+ // errors: https://github.com/facebookincubator/create-react-app/issues/3482
+ // So we preemptively throw with a better message instead.
+ invariant(
+ typeof document !== 'undefined',
+ 'The `document` global was defined when React was initialized, but is not ' +
+ 'defined anymore. This can happen in a test environment if a component ' +
+ 'schedules an update from an asynchronous callback, but the test has already ' +
+ 'finished running. To solve this, you can either unmount the component at ' +
+ 'the end of your test (and ensure that any asynchronous operations get ' +
+ 'canceled in `componentWillUnmount`), or you can change the test itself ' +
+ 'to be asynchronous.',
+ );
+ const evt = document.createEvent('Event');
+
+ // Keeps track of whether the user-provided callback threw an error. We
+ // set this to true at the beginning, then set it to false right after
+ // calling the function. If the function errors, `didError` will never be
+ // set to false. This strategy works even if the browser is flaky and
+ // fails to call our global error handler, because it doesn't rely on
+ // the error event at all.
+ let didError = true;
+
+ // Create an event handler for our fake event. We will synchronously
+ // dispatch our fake event using `dispatchEvent`. Inside the handler, we
+ // call the user-provided callback.
+ const funcArgs = Array.prototype.slice.call(arguments, 3);
+ function callCallback() {
+ // We immediately remove the callback from event listeners so that
+ // nested `invokeGuardedCallback` calls do not clash. Otherwise, a
+ // nested call would trigger the fake event handlers of any call higher
+ // in the stack.
+ fakeNode.removeEventListener(evtType, callCallback, false);
+ func.apply(context, funcArgs);
+ didError = false;
+ }
+
+ // Create a global error event handler. We use this to capture the value
+ // that was thrown. It's possible that this error handler will fire more
+ // than once; for example, if non-React code also calls `dispatchEvent`
+ // and a handler for that event throws. We should be resilient to most of
+ // those cases. Even if our error event handler fires more than once, the
+ // last error event is always used. If the callback actually does error,
+ // we know that the last error event is the correct one, because it's not
+ // possible for anything else to have happened in between our callback
+ // erroring and the code that follows the `dispatchEvent` call below. If
+ // the callback doesn't error, but the error event was fired, we know to
+ // ignore it because `didError` will be false, as described above.
+ let error;
+ // Use this to track whether the error event is ever called.
+ let didSetError = false;
+ let isCrossOriginError = false;
+
+ function onError(event) {
+ error = event.error;
+ didSetError = true;
+ if (error === null && event.colno === 0 && event.lineno === 0) {
+ isCrossOriginError = true;
+ }
+ }
+
+ // Create a fake event type.
+ const evtType = `react-${name ? name : 'invokeguardedcallback'}`;
+
+ // Attach our event handlers
+ window.addEventListener('error', onError);
+ fakeNode.addEventListener(evtType, callCallback, false);
+
+ // Synchronously dispatch our fake event. If the user-provided function
+ // errors, it will trigger our global error handler.
+ evt.initEvent(evtType, false, false);
+ fakeNode.dispatchEvent(evt);
+
+ if (didError) {
+ if (!didSetError) {
+ // The callback errored, but the error event never fired.
+ error = new Error(
+ 'An error was thrown inside one of your components, but React ' +
+ "doesn't know what it was. This is likely due to browser " +
+ 'flakiness. React does its best to preserve the "Pause on ' +
+ 'exceptions" behavior of the DevTools, which requires some ' +
+ "DEV-mode only tricks. It's possible that these don't work in " +
+ 'your browser. Try triggering the error in production mode, ' +
+ 'or switching to a modern browser. If you suspect that this is ' +
+ 'actually an issue with React, please file an issue.',
+ );
+ } else if (isCrossOriginError) {
+ error = new Error(
+ "A cross-origin error was thrown. React doesn't have access to " +
+ 'the actual error object in development. ' +
+ 'See https://fb.me/react-crossorigin-error for more information.',
+ );
+ }
+ this._hasCaughtError = true;
+ this._caughtError = error;
+ } else {
+ this._hasCaughtError = false;
+ this._caughtError = null;
+ }
+
+ // Remove our event listeners
+ window.removeEventListener('error', onError);
+ };
+
+ invokeGuardedCallback = invokeGuardedCallbackDev;
+ }
+}
+
+export default invokeGuardedCallback;
diff --git a/scripts/rollup/forks.js b/scripts/rollup/forks.js
index 920b6a49e4f..fe8626bcfa8 100644
--- a/scripts/rollup/forks.js
+++ b/scripts/rollup/forks.js
@@ -69,7 +69,18 @@ const forks = Object.freeze({
}
},
- // Different behavior for caught errors.
+ // Different wrapping/reporting for caught errors.
+ 'shared/invokeGuardedCallback': (bundleType, entry) => {
+ switch (bundleType) {
+ case FB_DEV:
+ case FB_PROD:
+ return 'shared/forks/invokeGuardedCallback.www.js';
+ default:
+ return null;
+ }
+ },
+
+ // Different dialogs for caught errors.
'react-reconciler/src/ReactFiberErrorDialog': (bundleType, entry) => {
switch (bundleType) {
case FB_DEV: