2017-03-14 08:05:18 +08:00
|
|
|
/**
|
2017-09-25 04:48:13 +08:00
|
|
|
* Copyright (c) 2013-present, Facebook, Inc.
|
2013-11-09 08:53:52 +08:00
|
|
|
*
|
2017-09-25 04:48:13 +08:00
|
|
|
* This source code is licensed under the MIT license found in the
|
|
|
|
|
* LICENSE file in the root directory of this source tree.
|
2013-11-09 08:53:52 +08:00
|
|
|
*
|
2016-08-26 06:23:13 +08:00
|
|
|
* @flow
|
2013-11-09 08:53:52 +08:00
|
|
|
*/
|
|
|
|
|
|
2017-11-03 03:50:03 +08:00
|
|
|
import invariant from 'fbjs/lib/invariant';
|
2017-03-24 02:45:38 +08:00
|
|
|
|
2017-07-22 06:34:41 +08:00
|
|
|
const ReactErrorUtils = {
|
|
|
|
|
// Used by Fiber to simulate a try-catch.
|
[invokeGuardedCallback] Handle nested errors across separate renderers (#10270)
invokeGuardedCallback is a function we use in place of try-catch
statement. It accepts a function, and if the function throws, it
captures the error. In production, the implementation is a normal try-
catch. In development, we swap out the prod implementation for a special
version designed to preserve "Pause on all exceptions" behavior of the
browser DevTools.
invokeGuardedCallbackDev works by dispatching an event to a dummy DOM
node and calling the provided function inside a handler for that event.
We also attach an error event handler to the window object. If the
function throws, the global event handler is called and we can access
the error.
The global event handler is added and removed right before and after the
fake event is dispatched. But if invokeGuardedCallbackDev is nested --
that is, if it's invoked inside the body of another
invokeGuardedCallbackDev -- multiple error event handlers will attached
simultaneously. We only want the handler that corresponds to the deepest
level to handle the error. So we keep track of a depth counter, and
within the event handler, we only handle the error if the current depth
matches the depth at the time the function was invoked.
The problem that we discovered, and that this PR fixes, is that the
depth counter is local to each renderer. So if you nest separate copies
of invokeGuardedCallback from separate renderers, each renderer will
have its own depth counter, and multiple error handlers will fire for a
single, nested error.
2017-07-26 00:44:43 +08:00
|
|
|
_caughtError: (null: mixed),
|
|
|
|
|
_hasCaughtError: (false: boolean),
|
2017-03-24 03:13:07 +08:00
|
|
|
|
2017-07-22 06:34:41 +08:00
|
|
|
// Used by event system to capture/rethrow the first error.
|
[invokeGuardedCallback] Handle nested errors across separate renderers (#10270)
invokeGuardedCallback is a function we use in place of try-catch
statement. It accepts a function, and if the function throws, it
captures the error. In production, the implementation is a normal try-
catch. In development, we swap out the prod implementation for a special
version designed to preserve "Pause on all exceptions" behavior of the
browser DevTools.
invokeGuardedCallbackDev works by dispatching an event to a dummy DOM
node and calling the provided function inside a handler for that event.
We also attach an error event handler to the window object. If the
function throws, the global event handler is called and we can access
the error.
The global event handler is added and removed right before and after the
fake event is dispatched. But if invokeGuardedCallbackDev is nested --
that is, if it's invoked inside the body of another
invokeGuardedCallbackDev -- multiple error event handlers will attached
simultaneously. We only want the handler that corresponds to the deepest
level to handle the error. So we keep track of a depth counter, and
within the event handler, we only handle the error if the current depth
matches the depth at the time the function was invoked.
The problem that we discovered, and that this PR fixes, is that the
depth counter is local to each renderer. So if you nest separate copies
of invokeGuardedCallback from separate renderers, each renderer will
have its own depth counter, and multiple error handlers will fire for a
single, nested error.
2017-07-26 00:44:43 +08:00
|
|
|
_rethrowError: (null: mixed),
|
|
|
|
|
_hasRethrowError: (false: boolean),
|
2017-03-24 03:13:07 +08:00
|
|
|
|
2017-03-24 02:45:38 +08:00
|
|
|
injection: {
|
|
|
|
|
injectErrorUtils(injectedErrorUtils: Object) {
|
|
|
|
|
invariant(
|
|
|
|
|
typeof injectedErrorUtils.invokeGuardedCallback === 'function',
|
|
|
|
|
'Injected invokeGuardedCallback() must be a function.',
|
|
|
|
|
);
|
2017-03-24 03:13:07 +08:00
|
|
|
invokeGuardedCallback = injectedErrorUtils.invokeGuardedCallback;
|
2017-03-24 02:45:38 +08:00
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
|
2017-04-05 19:34:14 +08:00
|
|
|
/**
|
|
|
|
|
* Call a function while guarding against errors that happens within it.
|
|
|
|
|
* Returns an error if it throws, otherwise null.
|
2017-07-28 00:45:07 +08:00
|
|
|
*
|
2017-07-27 01:01:31 +08:00
|
|
|
* In production, this is implemented using a try-catch. The reason we don't
|
|
|
|
|
* use a try-catch directly is so that we can swap out a different
|
|
|
|
|
* implementation in DEV mode.
|
2017-04-05 19:34:14 +08:00
|
|
|
*
|
|
|
|
|
* @param {String} name of the guard to use for logging or debugging
|
|
|
|
|
* @param {Function} func The function to invoke
|
|
|
|
|
* @param {*} context The context to use when calling the function
|
|
|
|
|
* @param {...*} args Arguments for function
|
|
|
|
|
*/
|
2017-02-25 08:19:29 +08:00
|
|
|
invokeGuardedCallback: function<A, B, C, D, E, F, Context>(
|
|
|
|
|
name: string | null,
|
2017-03-02 06:38:29 +08:00
|
|
|
func: (a: A, b: B, c: C, d: D, e: E, f: F) => void,
|
2017-02-25 08:19:29 +08:00
|
|
|
context: Context,
|
|
|
|
|
a: A,
|
|
|
|
|
b: B,
|
|
|
|
|
c: C,
|
|
|
|
|
d: D,
|
|
|
|
|
e: E,
|
|
|
|
|
f: F,
|
2017-07-22 06:34:41 +08:00
|
|
|
): void {
|
|
|
|
|
invokeGuardedCallback.apply(ReactErrorUtils, arguments);
|
2017-02-25 08:19:29 +08:00
|
|
|
},
|
2015-09-23 06:29:52 +08:00
|
|
|
|
2013-11-09 08:53:52 +08:00
|
|
|
/**
|
2017-02-09 06:20:23 +08:00
|
|
|
* Same as invokeGuardedCallback, but instead of returning an error, it stores
|
|
|
|
|
* it in a global so it can be rethrown by `rethrowCaughtError` later.
|
2017-07-27 01:01:31 +08:00
|
|
|
* TODO: See if _caughtError and _rethrowError can be unified.
|
2017-02-09 06:20:23 +08:00
|
|
|
*
|
|
|
|
|
* @param {String} name of the guard to use for logging or debugging
|
|
|
|
|
* @param {Function} func The function to invoke
|
|
|
|
|
* @param {*} context The context to use when calling the function
|
|
|
|
|
* @param {...*} args Arguments for function
|
2015-06-20 13:31:40 +08:00
|
|
|
*/
|
2017-02-09 06:20:23 +08:00
|
|
|
invokeGuardedCallbackAndCatchFirstError: function<A, B, C, D, E, F, Context>(
|
|
|
|
|
name: string | null,
|
2017-03-02 06:38:29 +08:00
|
|
|
func: (a: A, b: B, c: C, d: D, e: E, f: F) => void,
|
2017-02-09 06:20:23 +08:00
|
|
|
context: Context,
|
|
|
|
|
a: A,
|
|
|
|
|
b: B,
|
|
|
|
|
c: C,
|
|
|
|
|
d: D,
|
|
|
|
|
e: E,
|
|
|
|
|
f: F,
|
|
|
|
|
): void {
|
2017-07-22 06:34:41 +08:00
|
|
|
ReactErrorUtils.invokeGuardedCallback.apply(this, arguments);
|
|
|
|
|
if (ReactErrorUtils.hasCaughtError()) {
|
|
|
|
|
const error = ReactErrorUtils.clearCaughtError();
|
|
|
|
|
if (!ReactErrorUtils._hasRethrowError) {
|
|
|
|
|
ReactErrorUtils._hasRethrowError = true;
|
|
|
|
|
ReactErrorUtils._rethrowError = error;
|
|
|
|
|
}
|
2017-02-09 06:20:23 +08:00
|
|
|
}
|
|
|
|
|
},
|
2015-06-20 13:31:40 +08:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* During execution of guarded functions we will capture the first error which
|
|
|
|
|
* we will rethrow to be handled by the top level error handler.
|
2013-11-09 08:53:52 +08:00
|
|
|
*/
|
2015-06-20 13:31:40 +08:00
|
|
|
rethrowCaughtError: function() {
|
2017-07-22 06:34:41 +08:00
|
|
|
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.',
|
|
|
|
|
);
|
|
|
|
|
}
|
2015-06-02 08:00:36 +08:00
|
|
|
},
|
2013-11-09 08:53:52 +08:00
|
|
|
};
|
|
|
|
|
|
2017-07-22 06:34:41 +08:00
|
|
|
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__) {
|
2017-07-27 01:01:31 +08:00
|
|
|
// 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!
|
2017-07-22 06:34:41 +08:00
|
|
|
|
2017-07-27 01:01:31 +08:00
|
|
|
// Check that the browser supports the APIs we need to implement our special
|
|
|
|
|
// DEV version of invokeGuardedCallback
|
2017-07-22 06:34:41 +08:00
|
|
|
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,
|
|
|
|
|
) {
|
2017-07-27 01:01:31 +08:00
|
|
|
// 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.
|
[invokeGuardedCallback] Handle nested errors across separate renderers (#10270)
invokeGuardedCallback is a function we use in place of try-catch
statement. It accepts a function, and if the function throws, it
captures the error. In production, the implementation is a normal try-
catch. In development, we swap out the prod implementation for a special
version designed to preserve "Pause on all exceptions" behavior of the
browser DevTools.
invokeGuardedCallbackDev works by dispatching an event to a dummy DOM
node and calling the provided function inside a handler for that event.
We also attach an error event handler to the window object. If the
function throws, the global event handler is called and we can access
the error.
The global event handler is added and removed right before and after the
fake event is dispatched. But if invokeGuardedCallbackDev is nested --
that is, if it's invoked inside the body of another
invokeGuardedCallbackDev -- multiple error event handlers will attached
simultaneously. We only want the handler that corresponds to the deepest
level to handle the error. So we keep track of a depth counter, and
within the event handler, we only handle the error if the current depth
matches the depth at the time the function was invoked.
The problem that we discovered, and that this PR fixes, is that the
depth counter is local to each renderer. So if you nest separate copies
of invokeGuardedCallback from separate renderers, each renderer will
have its own depth counter, and multiple error handlers will fire for a
single, nested error.
2017-07-26 00:44:43 +08:00
|
|
|
let didError = true;
|
2017-07-27 01:01:31 +08:00
|
|
|
|
|
|
|
|
// 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.
|
2017-07-22 06:34:41 +08:00
|
|
|
const funcArgs = Array.prototype.slice.call(arguments, 3);
|
2017-07-27 01:01:31 +08:00
|
|
|
function callCallback() {
|
2017-07-28 00:45:07 +08:00
|
|
|
// 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);
|
2017-07-22 06:34:41 +08:00
|
|
|
func.apply(context, funcArgs);
|
[invokeGuardedCallback] Handle nested errors across separate renderers (#10270)
invokeGuardedCallback is a function we use in place of try-catch
statement. It accepts a function, and if the function throws, it
captures the error. In production, the implementation is a normal try-
catch. In development, we swap out the prod implementation for a special
version designed to preserve "Pause on all exceptions" behavior of the
browser DevTools.
invokeGuardedCallbackDev works by dispatching an event to a dummy DOM
node and calling the provided function inside a handler for that event.
We also attach an error event handler to the window object. If the
function throws, the global event handler is called and we can access
the error.
The global event handler is added and removed right before and after the
fake event is dispatched. But if invokeGuardedCallbackDev is nested --
that is, if it's invoked inside the body of another
invokeGuardedCallbackDev -- multiple error event handlers will attached
simultaneously. We only want the handler that corresponds to the deepest
level to handle the error. So we keep track of a depth counter, and
within the event handler, we only handle the error if the current depth
matches the depth at the time the function was invoked.
The problem that we discovered, and that this PR fixes, is that the
depth counter is local to each renderer. So if you nest separate copies
of invokeGuardedCallback from separate renderers, each renderer will
have its own depth counter, and multiple error handlers will fire for a
single, nested error.
2017-07-26 00:44:43 +08:00
|
|
|
didError = false;
|
2017-07-27 01:01:31 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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;
|
Cross-origin error handling in DEV (#10353)
* Add DOM fixture for cross-origin errors
* Use a custom error object in place of cross-origin errors
Cross-origin errors aren't accessible by React in DEV mode because we
catch errors using a global error handler, in order to preserve the
"Pause on exceptions" behavior of the DevTools. When this happens, we
should use a custom error object that explains what happened.
For uncaught errors, the actual error message is logged to the console
by the browser, so React should skip logging the message again.
* Add test case that demonstrates errors are logged even if they're caught
* Don't double log error messages in DEV
In DEV, the browser always logs errors thrown inside React components,
even if the originating update is wrapped in a try-catch, because of the
dispatchEvent trick used by invokeGuardedCallback. So the error logger
should not log the message again.
* Fix tests
* Change how error is printed in DEV and PROD
In DEV, we don't want to print the stack trace because the browser already always prints it.
We'll just print the component stack now.
In PROD, we used to omit the JS error message. However we *do* want to show it because
if the application swallows the error, the browser will *not* print it. In DEV it works
only because of the fake event trick. So in PROD we will always print the underlying error
by logging the error object directly. This will show both the message and the JS stack.
* Make the wording tighter and emphasize the real error is above
There's a few goals in the rewording:
* Make it tighter using line breaks between sentences.
* Make it slightly less patronizing ("You should fix it" => "You can find its details in an earlier log")
* ^^ This also helps highlight that the real error message and stack is above
* Group subsections: intro (there's an error), component stack, and final addendum about error boundaries
* ^^ Otherwise people might think error boundaries are part of the reason they have an error
* Make it clear "located at" is not the stacktrace. Otherwise it feels confusing. This makes it clearer you should still look for stack trace (with other details) above and introduces the concept of component stack.
* Make the message shorter
* Unused variables
* Fix an error caused by fixing lint
* One more bikeshed
* Fix fixture
* Remove unused file
* Concise wording
* Unused variables
2017-08-04 01:25:53 +08:00
|
|
|
let isCrossOriginError = false;
|
2017-07-27 01:01:31 +08:00
|
|
|
|
|
|
|
|
function onError(event) {
|
[invokeGuardedCallback] Handle nested errors across separate renderers (#10270)
invokeGuardedCallback is a function we use in place of try-catch
statement. It accepts a function, and if the function throws, it
captures the error. In production, the implementation is a normal try-
catch. In development, we swap out the prod implementation for a special
version designed to preserve "Pause on all exceptions" behavior of the
browser DevTools.
invokeGuardedCallbackDev works by dispatching an event to a dummy DOM
node and calling the provided function inside a handler for that event.
We also attach an error event handler to the window object. If the
function throws, the global event handler is called and we can access
the error.
The global event handler is added and removed right before and after the
fake event is dispatched. But if invokeGuardedCallbackDev is nested --
that is, if it's invoked inside the body of another
invokeGuardedCallbackDev -- multiple error event handlers will attached
simultaneously. We only want the handler that corresponds to the deepest
level to handle the error. So we keep track of a depth counter, and
within the event handler, we only handle the error if the current depth
matches the depth at the time the function was invoked.
The problem that we discovered, and that this PR fixes, is that the
depth counter is local to each renderer. So if you nest separate copies
of invokeGuardedCallback from separate renderers, each renderer will
have its own depth counter, and multiple error handlers will fire for a
single, nested error.
2017-07-26 00:44:43 +08:00
|
|
|
error = event.error;
|
2017-07-27 01:01:31 +08:00
|
|
|
didSetError = true;
|
Cross-origin error handling in DEV (#10353)
* Add DOM fixture for cross-origin errors
* Use a custom error object in place of cross-origin errors
Cross-origin errors aren't accessible by React in DEV mode because we
catch errors using a global error handler, in order to preserve the
"Pause on exceptions" behavior of the DevTools. When this happens, we
should use a custom error object that explains what happened.
For uncaught errors, the actual error message is logged to the console
by the browser, so React should skip logging the message again.
* Add test case that demonstrates errors are logged even if they're caught
* Don't double log error messages in DEV
In DEV, the browser always logs errors thrown inside React components,
even if the originating update is wrapped in a try-catch, because of the
dispatchEvent trick used by invokeGuardedCallback. So the error logger
should not log the message again.
* Fix tests
* Change how error is printed in DEV and PROD
In DEV, we don't want to print the stack trace because the browser already always prints it.
We'll just print the component stack now.
In PROD, we used to omit the JS error message. However we *do* want to show it because
if the application swallows the error, the browser will *not* print it. In DEV it works
only because of the fake event trick. So in PROD we will always print the underlying error
by logging the error object directly. This will show both the message and the JS stack.
* Make the wording tighter and emphasize the real error is above
There's a few goals in the rewording:
* Make it tighter using line breaks between sentences.
* Make it slightly less patronizing ("You should fix it" => "You can find its details in an earlier log")
* ^^ This also helps highlight that the real error message and stack is above
* Group subsections: intro (there's an error), component stack, and final addendum about error boundaries
* ^^ Otherwise people might think error boundaries are part of the reason they have an error
* Make it clear "located at" is not the stacktrace. Otherwise it feels confusing. This makes it clearer you should still look for stack trace (with other details) above and introduces the concept of component stack.
* Make the message shorter
* Unused variables
* Fix an error caused by fixing lint
* One more bikeshed
* Fix fixture
* Remove unused file
* Concise wording
* Unused variables
2017-08-04 01:25:53 +08:00
|
|
|
if (error === null && event.colno === 0 && event.lineno === 0) {
|
|
|
|
|
isCrossOriginError = true;
|
|
|
|
|
}
|
2017-07-27 01:01:31 +08:00
|
|
|
}
|
[invokeGuardedCallback] Handle nested errors across separate renderers (#10270)
invokeGuardedCallback is a function we use in place of try-catch
statement. It accepts a function, and if the function throws, it
captures the error. In production, the implementation is a normal try-
catch. In development, we swap out the prod implementation for a special
version designed to preserve "Pause on all exceptions" behavior of the
browser DevTools.
invokeGuardedCallbackDev works by dispatching an event to a dummy DOM
node and calling the provided function inside a handler for that event.
We also attach an error event handler to the window object. If the
function throws, the global event handler is called and we can access
the error.
The global event handler is added and removed right before and after the
fake event is dispatched. But if invokeGuardedCallbackDev is nested --
that is, if it's invoked inside the body of another
invokeGuardedCallbackDev -- multiple error event handlers will attached
simultaneously. We only want the handler that corresponds to the deepest
level to handle the error. So we keep track of a depth counter, and
within the event handler, we only handle the error if the current depth
matches the depth at the time the function was invoked.
The problem that we discovered, and that this PR fixes, is that the
depth counter is local to each renderer. So if you nest separate copies
of invokeGuardedCallback from separate renderers, each renderer will
have its own depth counter, and multiple error handlers will fire for a
single, nested error.
2017-07-26 00:44:43 +08:00
|
|
|
|
2017-07-28 00:45:07 +08:00
|
|
|
// Create a fake event type.
|
|
|
|
|
const evtType = `react-${name ? name : 'invokeguardedcallback'}`;
|
[invokeGuardedCallback] Handle nested errors across separate renderers (#10270)
invokeGuardedCallback is a function we use in place of try-catch
statement. It accepts a function, and if the function throws, it
captures the error. In production, the implementation is a normal try-
catch. In development, we swap out the prod implementation for a special
version designed to preserve "Pause on all exceptions" behavior of the
browser DevTools.
invokeGuardedCallbackDev works by dispatching an event to a dummy DOM
node and calling the provided function inside a handler for that event.
We also attach an error event handler to the window object. If the
function throws, the global event handler is called and we can access
the error.
The global event handler is added and removed right before and after the
fake event is dispatched. But if invokeGuardedCallbackDev is nested --
that is, if it's invoked inside the body of another
invokeGuardedCallbackDev -- multiple error event handlers will attached
simultaneously. We only want the handler that corresponds to the deepest
level to handle the error. So we keep track of a depth counter, and
within the event handler, we only handle the error if the current depth
matches the depth at the time the function was invoked.
The problem that we discovered, and that this PR fixes, is that the
depth counter is local to each renderer. So if you nest separate copies
of invokeGuardedCallback from separate renderers, each renderer will
have its own depth counter, and multiple error handlers will fire for a
single, nested error.
2017-07-26 00:44:43 +08:00
|
|
|
|
2017-07-27 01:01:31 +08:00
|
|
|
// 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.
|
|
|
|
|
const evt = document.createEvent('Event');
|
2017-07-22 06:34:41 +08:00
|
|
|
evt.initEvent(evtType, false, false);
|
|
|
|
|
fakeNode.dispatchEvent(evt);
|
2017-07-27 01:01:31 +08:00
|
|
|
|
[invokeGuardedCallback] Handle nested errors across separate renderers (#10270)
invokeGuardedCallback is a function we use in place of try-catch
statement. It accepts a function, and if the function throws, it
captures the error. In production, the implementation is a normal try-
catch. In development, we swap out the prod implementation for a special
version designed to preserve "Pause on all exceptions" behavior of the
browser DevTools.
invokeGuardedCallbackDev works by dispatching an event to a dummy DOM
node and calling the provided function inside a handler for that event.
We also attach an error event handler to the window object. If the
function throws, the global event handler is called and we can access
the error.
The global event handler is added and removed right before and after the
fake event is dispatched. But if invokeGuardedCallbackDev is nested --
that is, if it's invoked inside the body of another
invokeGuardedCallbackDev -- multiple error event handlers will attached
simultaneously. We only want the handler that corresponds to the deepest
level to handle the error. So we keep track of a depth counter, and
within the event handler, we only handle the error if the current depth
matches the depth at the time the function was invoked.
The problem that we discovered, and that this PR fixes, is that the
depth counter is local to each renderer. So if you nest separate copies
of invokeGuardedCallback from separate renderers, each renderer will
have its own depth counter, and multiple error handlers will fire for a
single, nested error.
2017-07-26 00:44:43 +08:00
|
|
|
if (didError) {
|
2017-07-27 01:01:31 +08:00
|
|
|
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.',
|
|
|
|
|
);
|
Cross-origin error handling in DEV (#10353)
* Add DOM fixture for cross-origin errors
* Use a custom error object in place of cross-origin errors
Cross-origin errors aren't accessible by React in DEV mode because we
catch errors using a global error handler, in order to preserve the
"Pause on exceptions" behavior of the DevTools. When this happens, we
should use a custom error object that explains what happened.
For uncaught errors, the actual error message is logged to the console
by the browser, so React should skip logging the message again.
* Add test case that demonstrates errors are logged even if they're caught
* Don't double log error messages in DEV
In DEV, the browser always logs errors thrown inside React components,
even if the originating update is wrapped in a try-catch, because of the
dispatchEvent trick used by invokeGuardedCallback. So the error logger
should not log the message again.
* Fix tests
* Change how error is printed in DEV and PROD
In DEV, we don't want to print the stack trace because the browser already always prints it.
We'll just print the component stack now.
In PROD, we used to omit the JS error message. However we *do* want to show it because
if the application swallows the error, the browser will *not* print it. In DEV it works
only because of the fake event trick. So in PROD we will always print the underlying error
by logging the error object directly. This will show both the message and the JS stack.
* Make the wording tighter and emphasize the real error is above
There's a few goals in the rewording:
* Make it tighter using line breaks between sentences.
* Make it slightly less patronizing ("You should fix it" => "You can find its details in an earlier log")
* ^^ This also helps highlight that the real error message and stack is above
* Group subsections: intro (there's an error), component stack, and final addendum about error boundaries
* ^^ Otherwise people might think error boundaries are part of the reason they have an error
* Make it clear "located at" is not the stacktrace. Otherwise it feels confusing. This makes it clearer you should still look for stack trace (with other details) above and introduces the concept of component stack.
* Make the message shorter
* Unused variables
* Fix an error caused by fixing lint
* One more bikeshed
* Fix fixture
* Remove unused file
* Concise wording
* Unused variables
2017-08-04 01:25:53 +08:00
|
|
|
} else if (isCrossOriginError) {
|
|
|
|
|
error = new Error(
|
|
|
|
|
"A cross-origin error was thrown. React doesn't have access to " +
|
2017-08-15 02:05:29 +08:00
|
|
|
'the actual error object in development. ' +
|
|
|
|
|
'See https://fb.me/react-crossorigin-error for more information.',
|
Cross-origin error handling in DEV (#10353)
* Add DOM fixture for cross-origin errors
* Use a custom error object in place of cross-origin errors
Cross-origin errors aren't accessible by React in DEV mode because we
catch errors using a global error handler, in order to preserve the
"Pause on exceptions" behavior of the DevTools. When this happens, we
should use a custom error object that explains what happened.
For uncaught errors, the actual error message is logged to the console
by the browser, so React should skip logging the message again.
* Add test case that demonstrates errors are logged even if they're caught
* Don't double log error messages in DEV
In DEV, the browser always logs errors thrown inside React components,
even if the originating update is wrapped in a try-catch, because of the
dispatchEvent trick used by invokeGuardedCallback. So the error logger
should not log the message again.
* Fix tests
* Change how error is printed in DEV and PROD
In DEV, we don't want to print the stack trace because the browser already always prints it.
We'll just print the component stack now.
In PROD, we used to omit the JS error message. However we *do* want to show it because
if the application swallows the error, the browser will *not* print it. In DEV it works
only because of the fake event trick. So in PROD we will always print the underlying error
by logging the error object directly. This will show both the message and the JS stack.
* Make the wording tighter and emphasize the real error is above
There's a few goals in the rewording:
* Make it tighter using line breaks between sentences.
* Make it slightly less patronizing ("You should fix it" => "You can find its details in an earlier log")
* ^^ This also helps highlight that the real error message and stack is above
* Group subsections: intro (there's an error), component stack, and final addendum about error boundaries
* ^^ Otherwise people might think error boundaries are part of the reason they have an error
* Make it clear "located at" is not the stacktrace. Otherwise it feels confusing. This makes it clearer you should still look for stack trace (with other details) above and introduces the concept of component stack.
* Make the message shorter
* Unused variables
* Fix an error caused by fixing lint
* One more bikeshed
* Fix fixture
* Remove unused file
* Concise wording
* Unused variables
2017-08-04 01:25:53 +08:00
|
|
|
);
|
2017-07-27 01:01:31 +08:00
|
|
|
}
|
[invokeGuardedCallback] Handle nested errors across separate renderers (#10270)
invokeGuardedCallback is a function we use in place of try-catch
statement. It accepts a function, and if the function throws, it
captures the error. In production, the implementation is a normal try-
catch. In development, we swap out the prod implementation for a special
version designed to preserve "Pause on all exceptions" behavior of the
browser DevTools.
invokeGuardedCallbackDev works by dispatching an event to a dummy DOM
node and calling the provided function inside a handler for that event.
We also attach an error event handler to the window object. If the
function throws, the global event handler is called and we can access
the error.
The global event handler is added and removed right before and after the
fake event is dispatched. But if invokeGuardedCallbackDev is nested --
that is, if it's invoked inside the body of another
invokeGuardedCallbackDev -- multiple error event handlers will attached
simultaneously. We only want the handler that corresponds to the deepest
level to handle the error. So we keep track of a depth counter, and
within the event handler, we only handle the error if the current depth
matches the depth at the time the function was invoked.
The problem that we discovered, and that this PR fixes, is that the
depth counter is local to each renderer. So if you nest separate copies
of invokeGuardedCallback from separate renderers, each renderer will
have its own depth counter, and multiple error handlers will fire for a
single, nested error.
2017-07-26 00:44:43 +08:00
|
|
|
ReactErrorUtils._hasCaughtError = true;
|
|
|
|
|
ReactErrorUtils._caughtError = error;
|
|
|
|
|
} else {
|
|
|
|
|
ReactErrorUtils._hasCaughtError = false;
|
|
|
|
|
ReactErrorUtils._caughtError = null;
|
|
|
|
|
}
|
|
|
|
|
|
2017-07-27 01:01:31 +08:00
|
|
|
// Remove our event listeners
|
|
|
|
|
window.removeEventListener('error', onError);
|
2017-07-22 06:34:41 +08:00
|
|
|
};
|
|
|
|
|
|
2017-07-27 01:01:31 +08:00
|
|
|
invokeGuardedCallback = invokeGuardedCallbackDev;
|
2017-07-22 06:34:41 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let rethrowCaughtError = function() {
|
|
|
|
|
if (ReactErrorUtils._hasRethrowError) {
|
|
|
|
|
const error = ReactErrorUtils._rethrowError;
|
|
|
|
|
ReactErrorUtils._rethrowError = null;
|
|
|
|
|
ReactErrorUtils._hasRethrowError = false;
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2017-11-03 03:50:03 +08:00
|
|
|
export default ReactErrorUtils;
|