mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00: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.
This commit is contained in:
@@ -2334,7 +2334,8 @@ src/renderers/shared/utils/__tests__/ReactErrorUtils-test.js
|
||||
* 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)
|
||||
* handles nested errors (development)
|
||||
* handles nested errors in separate renderers
|
||||
* can be shimmed (development)
|
||||
* it should rethrow caught errors (production)
|
||||
* should call the callback the passed arguments (production)
|
||||
@@ -2342,7 +2343,8 @@ src/renderers/shared/utils/__tests__/ReactErrorUtils-test.js
|
||||
* 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)
|
||||
* handles nested errors (production)
|
||||
* handles nested errors in separate renderers
|
||||
* catches null values
|
||||
* can be shimmed (production)
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ if (__DEV__) {
|
||||
|
||||
module.exports = function<T, P, I, TI, PI, C, CX, PL>(
|
||||
config: HostConfig<T, P, I, TI, PI, C, CX, PL>,
|
||||
captureError: (failedFiber: Fiber, error: Error) => Fiber | null,
|
||||
captureError: (failedFiber: Fiber, error: mixed) => Fiber | null,
|
||||
) {
|
||||
const {
|
||||
commitMount,
|
||||
|
||||
@@ -16,12 +16,12 @@ const invariant = require('fbjs/lib/invariant');
|
||||
|
||||
const ReactErrorUtils = {
|
||||
// Used by Fiber to simulate a try-catch.
|
||||
_caughtError: null,
|
||||
_hasCaughtError: false,
|
||||
_caughtError: (null: mixed),
|
||||
_hasCaughtError: (false: boolean),
|
||||
|
||||
// Used by event system to capture/rethrow the first error.
|
||||
_rethrowError: null,
|
||||
_hasRethrowError: false,
|
||||
_rethrowError: (null: mixed),
|
||||
_hasRethrowError: (false: boolean),
|
||||
|
||||
injection: {
|
||||
injectErrorUtils(injectedErrorUtils: Object) {
|
||||
@@ -155,33 +155,40 @@ if (__DEV__) {
|
||||
e,
|
||||
f,
|
||||
) {
|
||||
ReactErrorUtils._hasCaughtError = false;
|
||||
ReactErrorUtils._caughtError = null;
|
||||
|
||||
depth++;
|
||||
const thisDepth = depth;
|
||||
|
||||
let error;
|
||||
let didError = true;
|
||||
const funcArgs = Array.prototype.slice.call(arguments, 3);
|
||||
const boundFunc = function() {
|
||||
func.apply(context, funcArgs);
|
||||
didError = false;
|
||||
};
|
||||
const onFakeEventError = function(event) {
|
||||
// Don't capture nested errors
|
||||
if (depth === thisDepth) {
|
||||
ReactErrorUtils._caughtError = event.error;
|
||||
ReactErrorUtils._hasCaughtError = true;
|
||||
}
|
||||
error = event.error;
|
||||
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);
|
||||
if (didError) {
|
||||
ReactErrorUtils._hasCaughtError = true;
|
||||
ReactErrorUtils._caughtError = error;
|
||||
} else {
|
||||
ReactErrorUtils._hasCaughtError = false;
|
||||
ReactErrorUtils._caughtError = null;
|
||||
}
|
||||
|
||||
fakeNode.removeEventListener(evtType, boundFunc, false);
|
||||
window.removeEventListener('error', onFakeEventError);
|
||||
|
||||
depth--;
|
||||
};
|
||||
|
||||
|
||||
@@ -132,7 +132,7 @@ describe('ReactErrorUtils', () => {
|
||||
expect(err4).toBe(err3);
|
||||
});
|
||||
|
||||
it(`does not return nested errors (${environment})`, () => {
|
||||
it(`handles nested errors (${environment})`, () => {
|
||||
const err1 = new Error();
|
||||
let err2;
|
||||
ReactErrorUtils.invokeGuardedCallback(
|
||||
@@ -155,6 +155,37 @@ describe('ReactErrorUtils', () => {
|
||||
expect(err2).toBe(err1);
|
||||
});
|
||||
|
||||
it('handles nested errors in separate renderers', () => {
|
||||
const ReactErrorUtils1 = require('ReactErrorUtils');
|
||||
jest.resetModules();
|
||||
const ReactErrorUtils2 = require('ReactErrorUtils');
|
||||
expect(ReactErrorUtils1).not.toEqual(ReactErrorUtils2);
|
||||
|
||||
let ops = [];
|
||||
|
||||
ReactErrorUtils1.invokeGuardedCallback(
|
||||
null,
|
||||
() => {
|
||||
ReactErrorUtils2.invokeGuardedCallback(
|
||||
null,
|
||||
() => {
|
||||
throw new Error('nested error');
|
||||
},
|
||||
null,
|
||||
);
|
||||
// ReactErrorUtils2 should catch the error
|
||||
ops.push(ReactErrorUtils2.hasCaughtError());
|
||||
ops.push(ReactErrorUtils2.clearCaughtError().message);
|
||||
},
|
||||
null,
|
||||
);
|
||||
|
||||
// ReactErrorUtils1 should not catch the error
|
||||
ops.push(ReactErrorUtils1.hasCaughtError());
|
||||
|
||||
expect(ops).toEqual([true, 'nested error', false]);
|
||||
});
|
||||
|
||||
if (environment === 'production') {
|
||||
// jsdom doesn't handle this properly, but Chrome and Firefox should. Test
|
||||
// this with a fixture.
|
||||
|
||||
Reference in New Issue
Block a user