Skip to content

Commit 0304008

Browse files
committed
invokeGuardedCallback returns a thrown error
We can use this more flexible version for error handling in Fiber. The DEV mode version uses same fake event trick as before to preserve "break on uncaught exception" behavior when debugging.
1 parent 0934ab9 commit 0304008

File tree

6 files changed

+136
-64
lines changed

6 files changed

+136
-64
lines changed

scripts/fiber/tests-passing.txt

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1667,11 +1667,16 @@ src/renderers/shared/stack/reconciler/__tests__/Transaction-test.js
16671667
* should allow nesting of transactions
16681668

16691669
src/renderers/shared/utils/__tests__/ReactErrorUtils-test.js
1670-
* should call the callback with only the passed argument
1671-
* should catch errors
1670+
* should call the callback the passed arguments
1671+
* should call the callback with the provided context
1672+
* should return a caught error
1673+
* should return null if no error is thrown
16721674
* should rethrow caught errors
1673-
* should call the callback with only the passed argument
1674-
* should use invokeGuardedCallbackWithCatch in production
1675+
* should call the callback the passed arguments
1676+
* should call the callback with the provided context
1677+
* should return a caught error
1678+
* should return null if no error is thrown
1679+
* should use invokeGuardedCallbackProd in production
16751680

16761681
src/renderers/shared/utils/__tests__/accumulateInto-test.js
16771682
* throws if the second item is null

src/renderers/native/__tests__/ReactNativeEvents-test.js

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313

1414
var RCTEventEmitter;
1515
var React;
16-
var ReactErrorUtils;
1716
var ReactNative;
1817
var ResponderEventPlugin;
1918
var UIManager;
@@ -24,16 +23,10 @@ beforeEach(() => {
2423

2524
RCTEventEmitter = require('RCTEventEmitter');
2625
React = require('React');
27-
ReactErrorUtils = require('ReactErrorUtils');
2826
ReactNative = require('ReactNative');
2927
ResponderEventPlugin = require('ResponderEventPlugin');
3028
UIManager = require('UIManager');
3129
createReactNativeComponentClass = require('createReactNativeComponentClass');
32-
33-
// Ensure errors from event callbacks are properly surfaced (otherwise,
34-
// jest/jsdom swallows them when we do the .dispatchEvent call)
35-
ReactErrorUtils.invokeGuardedCallback =
36-
ReactErrorUtils.invokeGuardedCallbackWithCatch;
3730
});
3831

3932
it('handles events', () => {

src/renderers/shared/shared/event/EventPluginUtils.js

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -89,15 +89,7 @@ if (__DEV__) {
8989
function executeDispatch(event, simulated, listener, inst) {
9090
var type = event.type || 'unknown-event';
9191
event.currentTarget = EventPluginUtils.getNodeFromInstance(inst);
92-
if (simulated) {
93-
ReactErrorUtils.invokeGuardedCallbackWithCatch(
94-
type,
95-
listener,
96-
event
97-
);
98-
} else {
99-
ReactErrorUtils.invokeGuardedCallback(type, listener, event);
100-
}
92+
ReactErrorUtils.invokeGuardedCallbackAndCatchFirstError(type, listener, undefined, event);
10193
event.currentTarget = null;
10294
}
10395

src/renderers/shared/stack/reconciler/ReactCompositeComponent.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -557,7 +557,7 @@ var ReactCompositeComponent = {
557557
if (safely) {
558558
if (!skipLifecycle) {
559559
var name = this.getName() + '.componentWillUnmount()';
560-
ReactErrorUtils.invokeGuardedCallback(name, inst.componentWillUnmount.bind(inst));
560+
ReactErrorUtils.invokeGuardedCallbackAndCatchFirstError(name, inst.componentWillUnmount, inst);
561561
}
562562
} else {
563563
if (__DEV__) {

src/renderers/shared/utils/ReactErrorUtils.js

Lines changed: 71 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -16,33 +16,62 @@ var caughtError = null;
1616

1717
/**
1818
* Call a function while guarding against errors that happens within it.
19+
* Returns an error if it throws, otherwise null.
1920
*
2021
* @param {String} name of the guard to use for logging or debugging
2122
* @param {Function} func The function to invoke
22-
* @param {*} a Argument
23+
* @param {*} context The context to use when calling the function
24+
* @param {...*} args Arguments for function
2325
*/
24-
function invokeGuardedCallback<A>(
25-
name: string,
26-
func: (a: A) => void,
26+
function invokeGuardedCallback<A, B, C, D, E, F, Context>(
27+
name: string | null,
28+
func: (A, B, C, D, E, F) => void,
29+
context: Context,
2730
a: A,
28-
): void {
31+
b: B,
32+
c: C,
33+
d: D,
34+
e: E,
35+
f: F,
36+
): Error | null {
37+
const funcArgs = Array.prototype.slice.call(arguments, 3);
2938
try {
30-
func(a);
31-
} catch (x) {
32-
if (caughtError === null) {
33-
caughtError = x;
34-
}
39+
func.apply(context, funcArgs);
40+
} catch (error) {
41+
return error;
3542
}
43+
return null;
3644
}
3745

3846
var ReactErrorUtils = {
39-
invokeGuardedCallback: invokeGuardedCallback,
47+
invokeGuardedCallback,
48+
invokeGuardedCallbackProd: invokeGuardedCallback,
4049

4150
/**
42-
* Invoked by ReactTestUtils.Simulate so that any errors thrown by the event
43-
* handler are sure to be rethrown by rethrowCaughtError.
51+
* Same as invokeGuardedCallback, but instead of returning an error, it stores
52+
* it in a global so it can be rethrown by `rethrowCaughtError` later.
53+
*
54+
* @param {String} name of the guard to use for logging or debugging
55+
* @param {Function} func The function to invoke
56+
* @param {*} context The context to use when calling the function
57+
* @param {...*} args Arguments for function
4458
*/
45-
invokeGuardedCallbackWithCatch: invokeGuardedCallback,
59+
invokeGuardedCallbackAndCatchFirstError: function<A, B, C, D, E, F, Context>(
60+
name: string | null,
61+
func: (A, B, C, D, E, F) => void,
62+
context: Context,
63+
a: A,
64+
b: B,
65+
c: C,
66+
d: D,
67+
e: E,
68+
f: F,
69+
): void {
70+
const error = ReactErrorUtils.invokeGuardedCallback.apply(this, arguments);
71+
if (error !== null && caughtError === null) {
72+
caughtError = error;
73+
}
74+
},
4675

4776
/**
4877
* During execution of guarded functions we will capture the first error which
@@ -66,21 +95,39 @@ if (__DEV__) {
6695
typeof window.dispatchEvent === 'function' &&
6796
typeof document !== 'undefined' &&
6897
typeof document.createEvent === 'function') {
69-
var fakeNode = document.createElement('react');
70-
ReactErrorUtils.invokeGuardedCallback = function<A>(
71-
name: string,
72-
func: (a: A) => void,
73-
a: A,
74-
): void {
75-
var boundFunc = function() {
76-
func(a);
98+
99+
const fakeNode = document.createElement('react');
100+
let fakeEventError = null;
101+
const onFakeEventError = function(event) {
102+
fakeEventError = event.error;
103+
};
104+
105+
ReactErrorUtils.invokeGuardedCallback = function(
106+
name,
107+
func,
108+
context,
109+
a,
110+
b,
111+
c,
112+
d,
113+
e,
114+
f
115+
) {
116+
const funcArgs = Array.prototype.slice.call(arguments, 3);
117+
const boundFunc = function() {
118+
func.apply(context, funcArgs);
77119
};
78-
var evtType = `react-${name}`;
120+
const evtType = 'react-' + (name ? name : 'invokeguardedcallback');
121+
window.addEventListener('error', onFakeEventError);
79122
fakeNode.addEventListener(evtType, boundFunc, false);
80-
var evt = document.createEvent('Event');
123+
const evt = document.createEvent('Event');
81124
evt.initEvent(evtType, false, false);
82125
fakeNode.dispatchEvent(evt);
83126
fakeNode.removeEventListener(evtType, boundFunc, false);
127+
window.removeEventListener('error', onFakeEventError);
128+
const returnVal = fakeEventError;
129+
fakeEventError = null;
130+
return returnVal;
84131
};
85132
}
86133
}

src/renderers/shared/utils/__tests__/ReactErrorUtils-test.js

Lines changed: 54 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -19,44 +19,79 @@ describe('ReactErrorUtils', () => {
1919
ReactErrorUtils = require('ReactErrorUtils');
2020
});
2121

22-
describe('invokeGuardedCallbackWithCatch', () => {
23-
it('should call the callback with only the passed argument', () => {
22+
describe('invokeGuardedCallbackProd', () => {
23+
it('should call the callback the passed arguments', () => {
2424
var callback = jest.fn();
25-
ReactErrorUtils.invokeGuardedCallbackWithCatch('foo', callback, 'arg');
26-
expect(callback).toBeCalledWith('arg');
25+
ReactErrorUtils.invokeGuardedCallbackProd('foo', callback, null, 'arg1', 'arg2');
26+
expect(callback).toBeCalledWith('arg1', 'arg2');
2727
});
2828

29-
it('should catch errors', () => {
30-
var callback = function() {
31-
throw new Error('foo');
32-
};
33-
expect(
34-
() => ReactErrorUtils.invokeGuardedCallbackWithCatch('foo', callback)
35-
).not.toThrow();
29+
it('should call the callback with the provided context', () => {
30+
var context = { didCall: false };
31+
ReactErrorUtils.invokeGuardedCallbackProd('foo', function() {
32+
this.didCall = true;
33+
}, context);
34+
expect(context.didCall).toBe(true);
35+
});
36+
37+
it('should return a caught error', () => {
38+
const error = new Error();
39+
const returnValue = ReactErrorUtils.invokeGuardedCallbackProd('foo', function() {
40+
throw error;
41+
}, null, 'arg1', 'arg2');
42+
expect(returnValue).toBe(error);
43+
});
44+
45+
it('should return null if no error is thrown', () => {
46+
var callback = jest.fn();
47+
const returnValue = ReactErrorUtils.invokeGuardedCallbackProd('foo', callback, null);
48+
expect(returnValue).toBe(null);
3649
});
3750
});
3851

39-
describe('rethrowCaughtError', () => {
52+
describe('invokeGuardedCallbackAndCatchFirstError', () => {
4053
it('should rethrow caught errors', () => {
4154
var err = new Error('foo');
4255
var callback = function() {
4356
throw err;
4457
};
45-
ReactErrorUtils.invokeGuardedCallbackWithCatch('foo', callback);
58+
ReactErrorUtils.invokeGuardedCallbackAndCatchFirstError('foo', callback, null);
4659
expect(() => ReactErrorUtils.rethrowCaughtError()).toThrow(err);
4760
});
4861
});
4962

5063
describe('invokeGuardedCallback', () => {
51-
it('should call the callback with only the passed argument', () => {
64+
it('should call the callback the passed arguments', () => {
65+
var callback = jest.fn();
66+
ReactErrorUtils.invokeGuardedCallback('foo', callback, null, 'arg1', 'arg2');
67+
expect(callback).toBeCalledWith('arg1', 'arg2');
68+
});
69+
70+
it('should call the callback with the provided context', () => {
71+
var context = { didCall: false };
72+
ReactErrorUtils.invokeGuardedCallback('foo', function() {
73+
this.didCall = true;
74+
}, context);
75+
expect(context.didCall).toBe(true);
76+
});
77+
78+
it('should return a caught error', () => {
79+
const error = new Error();
80+
const returnValue = ReactErrorUtils.invokeGuardedCallback('foo', function() {
81+
throw error;
82+
}, null, 'arg1', 'arg2');
83+
expect(returnValue).toBe(error);
84+
});
85+
86+
it('should return null if no error is thrown', () => {
5287
var callback = jest.fn();
53-
ReactErrorUtils.invokeGuardedCallback('foo', callback, 'arg');
54-
expect(callback).toBeCalledWith('arg');
88+
const returnValue = ReactErrorUtils.invokeGuardedCallback('foo', callback, null);
89+
expect(returnValue).toBe(null);
5590
});
5691

57-
it('should use invokeGuardedCallbackWithCatch in production', () => {
92+
it('should use invokeGuardedCallbackProd in production', () => {
5893
expect(ReactErrorUtils.invokeGuardedCallback).not.toEqual(
59-
ReactErrorUtils.invokeGuardedCallbackWithCatch
94+
ReactErrorUtils.invokeGuardedCallbackProd
6095
);
6196
__DEV__ = false;
6297
var oldProcess = process;
@@ -66,7 +101,7 @@ describe('ReactErrorUtils', () => {
66101
jest.resetModules();
67102
ReactErrorUtils = require('ReactErrorUtils');
68103
expect(ReactErrorUtils.invokeGuardedCallback).toEqual(
69-
ReactErrorUtils.invokeGuardedCallbackWithCatch
104+
ReactErrorUtils.invokeGuardedCallbackProd
70105
);
71106
__DEV__ = true;
72107
global.process = oldProcess;

0 commit comments

Comments
 (0)