From 944275e50fd3a5dab9972868d715ae8ef9043ad3 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Sat, 9 Mar 2024 18:07:40 -0800 Subject: [PATCH] Inline ReactErrorUtils This is pretty simple now. --- .../src/events/DOMPluginEventSystem.js | 26 +++- .../src/__tests__/ReactDOMFizzServer-test.js | 4 +- .../src/test-utils/ReactTestUtils.js | 24 +++- .../src/legacy-events/EventBatching.js | 4 +- .../src/legacy-events/EventPluginUtils.js | 25 +++- packages/shared/ReactErrorUtils.js | 130 ------------------ 6 files changed, 61 insertions(+), 152 deletions(-) delete mode 100644 packages/shared/ReactErrorUtils.js diff --git a/packages/react-dom-bindings/src/events/DOMPluginEventSystem.js b/packages/react-dom-bindings/src/events/DOMPluginEventSystem.js index ca89e1d1bd8ab..5bb1fdc91150e 100644 --- a/packages/react-dom-bindings/src/events/DOMPluginEventSystem.js +++ b/packages/react-dom-bindings/src/events/DOMPluginEventSystem.js @@ -55,10 +55,6 @@ import { enableFloat, enableFormActions, } from 'shared/ReactFeatureFlags'; -import { - invokeGuardedCallbackAndCatchFirstError, - rethrowCaughtError, -} from 'shared/ReactErrorUtils'; import {createEventListenerWrapperWithPriority} from './ReactDOMEventListener'; import { removeEventListener, @@ -234,14 +230,25 @@ export const nonDelegatedEvents: Set = new Set([ ...mediaEventTypes, ]); +let hasError: boolean = false; +let caughtError: mixed = null; + function executeDispatch( event: ReactSyntheticEvent, listener: Function, currentTarget: EventTarget, ): void { - const type = event.type || 'unknown-event'; event.currentTarget = currentTarget; - invokeGuardedCallbackAndCatchFirstError(type, listener, undefined, event); + try { + listener(event); + } catch (error) { + if (!hasError) { + hasError = true; + caughtError = error; + } else { + // TODO: Make sure this error gets logged somehow. + } + } event.currentTarget = null; } @@ -283,7 +290,12 @@ export function processDispatchQueue( // event system doesn't use pooling. } // This would be a good time to rethrow if any of the event handlers threw. - rethrowCaughtError(); + if (hasError) { + const error = caughtError; + hasError = false; + caughtError = null; + throw error; + } } function dispatchEventsForPlugins( diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js index f2734d3175a49..e6ff3e2c22089 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js @@ -4643,7 +4643,7 @@ describe('ReactDOMFizzServer', () => { await waitForAll([]); }); - it('does not invokeGuardedCallback for errors after the first hydration error', async () => { + it('does not log for errors after the first hydration error', async () => { // We can't use the toErrorDev helper here because this is async. const originalConsoleError = console.error; const mockError = jest.fn(); @@ -4740,7 +4740,7 @@ describe('ReactDOMFizzServer', () => { } }); - it('does not invokeGuardedCallback for errors after a preceding fiber suspends', async () => { + it('does not log for errors after a preceding fiber suspends', async () => { // We can't use the toErrorDev helper here because this is async. const originalConsoleError = console.error; const mockError = jest.fn(); diff --git a/packages/react-dom/src/test-utils/ReactTestUtils.js b/packages/react-dom/src/test-utils/ReactTestUtils.js index c2af208a0a6a0..8a4f2eb9ba7cd 100644 --- a/packages/react-dom/src/test-utils/ReactTestUtils.js +++ b/packages/react-dom/src/test-utils/ReactTestUtils.js @@ -21,10 +21,6 @@ import { } from 'react-reconciler/src/ReactWorkTags'; import {SyntheticEvent} from 'react-dom-bindings/src/events/SyntheticEvent'; import {ELEMENT_NODE} from 'react-dom-bindings/src/client/HTMLNodeType'; -import { - rethrowCaughtError, - invokeGuardedCallbackAndCatchFirstError, -} from 'shared/ReactErrorUtils'; import {enableFloat} from 'shared/ReactFeatureFlags'; import assign from 'shared/assign'; import isArray from 'shared/isArray'; @@ -354,6 +350,9 @@ function nativeTouchData(x, y) { // EventPropagator.js, as they deviated from ReactDOM's newer // implementations. +let hasError: boolean = false; +let caughtError: mixed = null; + /** * Dispatch the event to the listener. * @param {SyntheticEvent} event SyntheticEvent to handle @@ -361,9 +360,15 @@ function nativeTouchData(x, y) { * @param {*} inst Internal component instance */ function executeDispatch(event, listener, inst) { - const type = event.type || 'unknown-event'; event.currentTarget = getNodeFromInstance(inst); - invokeGuardedCallbackAndCatchFirstError(type, listener, undefined, event); + try { + listener(event); + } catch (error) { + if (!hasError) { + hasError = true; + caughtError = error; + } + } event.currentTarget = null; } @@ -619,7 +624,12 @@ function makeSimulator(eventType) { // do that since we're by-passing it here. enqueueStateRestore(domNode); executeDispatchesAndRelease(event); - rethrowCaughtError(); + if (hasError) { + const error = caughtError; + hasError = false; + caughtError = null; + throw error; + } }); restoreStateIfNeeded(); }; diff --git a/packages/react-native-renderer/src/legacy-events/EventBatching.js b/packages/react-native-renderer/src/legacy-events/EventBatching.js index cad3671723ed7..4a65042fc2e7b 100644 --- a/packages/react-native-renderer/src/legacy-events/EventBatching.js +++ b/packages/react-native-renderer/src/legacy-events/EventBatching.js @@ -6,12 +6,10 @@ * @flow */ -import {rethrowCaughtError} from 'shared/ReactErrorUtils'; - import type {ReactSyntheticEvent} from './ReactSyntheticEventType'; import accumulateInto from './accumulateInto'; import forEachAccumulated from './forEachAccumulated'; -import {executeDispatchesInOrder} from './EventPluginUtils'; +import {executeDispatchesInOrder, rethrowCaughtError} from './EventPluginUtils'; /** * Internal queue of events that have accumulated their dispatches and are diff --git a/packages/react-native-renderer/src/legacy-events/EventPluginUtils.js b/packages/react-native-renderer/src/legacy-events/EventPluginUtils.js index d61bd71ee0ea8..f97d7b5488b50 100644 --- a/packages/react-native-renderer/src/legacy-events/EventPluginUtils.js +++ b/packages/react-native-renderer/src/legacy-events/EventPluginUtils.js @@ -5,9 +5,11 @@ * LICENSE file in the root directory of this source tree. */ -import {invokeGuardedCallbackAndCatchFirstError} from 'shared/ReactErrorUtils'; import isArray from 'shared/isArray'; +let hasError = false; +let caughtError = null; + export let getFiberCurrentPropsFromNode = null; export let getInstanceFromNode = null; export let getNodeFromInstance = null; @@ -62,9 +64,17 @@ function validateEventDispatches(event) { * @param {*} inst Internal component instance */ export function executeDispatch(event, listener, inst) { - const type = event.type || 'unknown-event'; event.currentTarget = getNodeFromInstance(inst); - invokeGuardedCallbackAndCatchFirstError(type, listener, undefined, event); + try { + listener(event); + } catch (error) { + if (!hasError) { + hasError = true; + caughtError = error; + } else { + // TODO: Make sure this error gets logged somehow. + } + } event.currentTarget = null; } @@ -170,3 +180,12 @@ export function executeDirectDispatch(event) { export function hasDispatches(event) { return !!event._dispatchListeners; } + +export function rethrowCaughtError() { + if (hasError) { + const error = caughtError; + hasError = false; + caughtError = null; + throw error; + } +} diff --git a/packages/shared/ReactErrorUtils.js b/packages/shared/ReactErrorUtils.js deleted file mode 100644 index b5a7982c84bd9..0000000000000 --- a/packages/shared/ReactErrorUtils.js +++ /dev/null @@ -1,130 +0,0 @@ -/** - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow - */ - -// Used by Fiber to simulate a try-catch. -let hasError: boolean = false; -let caughtError: mixed = null; - -// Used by event system to capture/rethrow the first error. -let hasRethrowError: boolean = false; -let rethrowError: mixed = null; - -/** - * Call a function while guarding against errors that happens within it. - * Returns an error if it throws, otherwise null. - * - * 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. - * - * @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 - */ -export function invokeGuardedCallback( - name: string | null, - func: (a: A, b: B, c: C, d: D, e: E, f: F) => mixed, - context: Context, - a: A, - b: B, - c: C, - d: D, - e: E, - f: F, -): void { - hasError = false; - caughtError = null; - try { - // $FlowFixMe[method-unbinding] - const funcArgs = Array.prototype.slice.call(arguments, 3); - func.apply(context, funcArgs); - } catch (error) { - hasError = true; - caughtError = error; - } -} - -/** - * Same as invokeGuardedCallback, but instead of returning an error, it stores - * it in a global so it can be rethrown by `rethrowCaughtError` later. - * TODO: See if caughtError and rethrowError can be unified. - * - * @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 - */ -export function invokeGuardedCallbackAndCatchFirstError< - A, - B, - C, - D, - E, - F, - Context, ->( - this: mixed, - 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, -): void { - try { - // $FlowFixMe[method-unbinding] - const funcArgs = Array.prototype.slice.call(arguments, 3); - func.apply(context, funcArgs); - } catch (error) { - hasError = true; - caughtError = error; - } - if (hasError) { - const error = clearCaughtError(); - if (!hasRethrowError) { - hasRethrowError = true; - rethrowError = error; - } - } -} - -/** - * During execution of guarded functions we will capture the first error which - * we will rethrow to be handled by the top level error handler. - */ -export function rethrowCaughtError() { - if (hasRethrowError) { - const error = rethrowError; - hasRethrowError = false; - rethrowError = null; - throw error; - } -} - -export function hasCaughtError(): boolean { - return hasError; -} - -export function clearCaughtError(): mixed { - if (hasError) { - const error = caughtError; - hasError = false; - caughtError = null; - return error; - } else { - throw new Error( - 'clearCaughtError was called but no error was captured. This error ' + - 'is likely caused by a bug in React. Please file an issue.', - ); - } -}