77 * @flow
88 */
99
10- // $FlowFixMe[missing-this-annot]
11- function invokeGuardedCallbackProd < Args : Array < mixed > , Context> (
12- name : string | null ,
13- func : ( ...Args ) => mixed ,
14- context : Context ,
15- ) : void {
16- // $FlowFixMe[method-unbinding]
17- const funcArgs = Array . prototype . slice . call ( arguments , 3 ) ;
18- try {
19- // $FlowFixMe[incompatible-call] Flow doesn't understand the arguments splicing.
20- func . apply ( context , funcArgs ) ;
21- } catch ( error ) {
22- this . onError ( error ) ;
23- }
24- }
25-
26- let invokeGuardedCallbackImpl : < Args : Array < mixed > , Context> (
27- name : string | null ,
28- func : ( ...Args ) => mixed ,
29- context : Context ,
30- ) => void = invokeGuardedCallbackProd ;
31-
10+ let fakeNode : Element = ( null : any ) ;
3211if ( __DEV__ ) {
33- // In DEV mode, we swap out invokeGuardedCallback for a special version
34- // that plays more nicely with the browser's DevTools. The idea is to preserve
35- // "Pause on exceptions" behavior. Because React wraps all user-provided
36- // functions in invokeGuardedCallback, and the production version of
37- // invokeGuardedCallback uses a try-catch, all user exceptions are treated
38- // like caught exceptions, and the DevTools won't pause unless the developer
39- // takes the extra step of enabling pause on caught exceptions. This is
40- // unintuitive, though, because even though React has caught the error, from
41- // the developer's perspective, the error is uncaught.
42- //
43- // To preserve the expected "Pause on exceptions" behavior, we don't use a
44- // try-catch in DEV. Instead, we synchronously dispatch a fake event to a fake
45- // DOM node, and call the user-provided callback from inside an event handler
46- // for that fake event. If the callback throws, the error is "captured" using
47- // a global event handler. But because the error happens in a different
48- // event loop context, it does not interrupt the normal program flow.
49- // Effectively, this gives us try-catch behavior without actually using
50- // try-catch. Neat!
51-
52- // Check that the browser supports the APIs we need to implement our special
53- // DEV version of invokeGuardedCallback
5412 if (
5513 typeof window !== 'undefined' &&
5614 typeof window . dispatchEvent === 'function' &&
5715 typeof document !== 'undefined' &&
5816 // $FlowFixMe[method-unbinding]
5917 typeof document . createEvent === 'function'
6018 ) {
61- const fakeNode = document . createElement ( 'react' ) ;
62-
63- invokeGuardedCallbackImpl = function invokeGuardedCallbackDev <
64- Args : Array < mixed > ,
65- Context,
66- // $FlowFixMe[missing-this-annot]
67- > ( name : string | null , func : ( ...Args ) => mixed , context : Context ) : void {
68- // If document doesn't exist we know for sure we will crash in this method
69- // when we call document.createEvent(). However this can cause confusing
70- // errors: https://github.com/facebook/create-react-app/issues/3482
71- // So we preemptively throw with a better message instead.
72- if ( typeof document === 'undefined' || document === null ) {
73- throw new Error (
74- 'The `document` global was defined when React was initialized, but is not ' +
75- 'defined anymore. This can happen in a test environment if a component ' +
76- 'schedules an update from an asynchronous callback, but the test has already ' +
77- 'finished running. To solve this, you can either unmount the component at ' +
78- 'the end of your test (and ensure that any asynchronous operations get ' +
79- 'canceled in `componentWillUnmount`), or you can change the test itself ' +
80- 'to be asynchronous.' ,
81- ) ;
82- }
19+ fakeNode = document . createElement ( 'react' ) ;
20+ }
21+ }
8322
23+ export default function invokeGuardedCallbackImpl < Args : Array < mixed > , Context> (
24+ this : { onError : ( error : mixed ) => void } ,
25+ name : string | null ,
26+ func : ( ...Args ) => mixed ,
27+ context : Context ,
28+ ) : void {
29+ if ( __DEV__ ) {
30+ // In DEV mode, we use a special version
31+ // that plays more nicely with the browser's DevTools. The idea is to preserve
32+ // "Pause on exceptions" behavior. Because React wraps all user-provided
33+ // functions in invokeGuardedCallback, and the production version of
34+ // invokeGuardedCallback uses a try-catch, all user exceptions are treated
35+ // like caught exceptions, and the DevTools won't pause unless the developer
36+ // takes the extra step of enabling pause on caught exceptions. This is
37+ // unintuitive, though, because even though React has caught the error, from
38+ // the developer's perspective, the error is uncaught.
39+ //
40+ // To preserve the expected "Pause on exceptions" behavior, we don't use a
41+ // try-catch in DEV. Instead, we synchronously dispatch a fake event to a fake
42+ // DOM node, and call the user-provided callback from inside an event handler
43+ // for that fake event. If the callback throws, the error is "captured" using
44+ // event loop context, it does not interrupt the normal program flow.
45+ // Effectively, this gives us try-catch behavior without actually using
46+ // try-catch. Neat!
47+
48+ // fakeNode signifies we are in an environment with a document and window object
49+ if ( fakeNode ) {
8450 const evt = document . createEvent ( 'Event' ) ;
8551
8652 let didCall = false ;
@@ -104,7 +70,7 @@ if (__DEV__) {
10470 'event' ,
10571 ) ;
10672
107- function restoreAfterDispatch ( ) {
73+ const restoreAfterDispatch = ( ) => {
10874 // We immediately remove the callback from event listeners so that
10975 // nested `invokeGuardedCallback` calls do not clash. Otherwise, a
11076 // nested call would trigger the fake event handlers of any call higher
@@ -121,20 +87,20 @@ if (__DEV__) {
12187 ) {
12288 window . event = windowEvent ;
12389 }
124- }
90+ } ;
12591
12692 // Create an event handler for our fake event. We will synchronously
12793 // dispatch our fake event using `dispatchEvent`. Inside the handler, we
12894 // call the user-provided callback.
12995 // $FlowFixMe[method-unbinding]
13096 const funcArgs = Array . prototype . slice . call ( arguments , 3 ) ;
131- function callCallback ( ) {
97+ const callCallback = ( ) => {
13298 didCall = true ;
13399 restoreAfterDispatch ( ) ;
134100 // $FlowFixMe[incompatible-call] Flow doesn't understand the arguments splicing.
135101 func . apply ( context , funcArgs ) ;
136102 didError = false ;
137- }
103+ } ;
138104
139105 // Create a global error event handler. We use this to capture the value
140106 // that was thrown. It's possible that this error handler will fire more
@@ -152,8 +118,7 @@ if (__DEV__) {
152118 let didSetError = false ;
153119 let isCrossOriginError = false ;
154120
155- // $FlowFixMe[missing-local-annot]
156- function handleWindowError ( event ) {
121+ const handleWindowError = ( event : ErrorEvent ) => {
157122 error = event . error ;
158123 didSetError = true ;
159124 if ( error === null && event . colno === 0 && event . lineno === 0 ) {
@@ -171,7 +136,7 @@ if (__DEV__) {
171136 }
172137 }
173138 }
174- }
139+ } ;
175140
176141 // Create a fake event type.
177142 const evtType = `react-${ name ? name : 'invokeguardedcallback' } ` ;
@@ -184,7 +149,6 @@ if (__DEV__) {
184149 // errors, it will trigger our global error handler.
185150 evt . initEvent ( evtType , false , false ) ;
186151 fakeNode . dispatchEvent ( evt ) ;
187-
188152 if ( windowEventDescriptor ) {
189153 Object . defineProperty ( window , 'event' , windowEventDescriptor ) ;
190154 }
@@ -217,16 +181,35 @@ if (__DEV__) {
217181 // Remove our event listeners
218182 window . removeEventListener ( 'error' , handleWindowError ) ;
219183
220- if ( ! didCall ) {
184+ if ( didCall ) {
185+ return ;
186+ } else {
221187 // Something went really wrong, and our event was not dispatched.
222188 // https://github.com/facebook/react/issues/16734
223189 // https://github.com/facebook/react/issues/16585
224190 // Fall back to the production implementation.
225191 restoreAfterDispatch ( ) ;
226- return invokeGuardedCallbackProd . apply ( this , arguments ) ;
192+ // we fall through and call the prod version instead
227193 }
228- } ;
194+ }
195+ // We only get here if we are in an environment that either does not support the browser
196+ // variant or we had trouble getting the browser to emit the error.
197+ // $FlowFixMe[method-unbinding]
198+ const funcArgs = Array . prototype . slice . call ( arguments , 3 ) ;
199+ try {
200+ // $FlowFixMe[incompatible-call] Flow doesn't understand the arguments splicing.
201+ func . apply ( context , funcArgs ) ;
202+ } catch ( error ) {
203+ this . onError ( error ) ;
204+ }
205+ } else {
206+ // $FlowFixMe[method-unbinding]
207+ const funcArgs = Array . prototype . slice . call ( arguments , 3 ) ;
208+ try {
209+ // $FlowFixMe[incompatible-call] Flow doesn't understand the arguments splicing.
210+ func. apply ( context , funcArgs ) ;
211+ } catch ( error ) {
212+ this . onError ( error ) ;
213+ }
229214 }
230215}
231-
232- export default invokeGuardedCallbackImpl ;
0 commit comments