mirror of
https://github.com/react/react-native.git
synced 2026-10-03 02:12:43 +08:00
Re-throw event listener errors in a new task (#56760)
Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/56760 Aligns error handling in the new EventTarget-based event dispatch path with the legacy plugin path, which surfaces handler errors to the host's global error handler (rather than swallowing them as `console.error`). Previously, when a React event handler threw, `EventTarget.invoke` caught the error and called `console.error(error)` — the error never reached the host's error reporter, so it was effectively silent in production builds that intercept `console.error`. This diff replaces the `console.error` calls in `EventTarget.invoke` with a small `reportListenerError` helper that schedules the error to be re-thrown in a new task via `setTimeout(0)`. The throw has no catcher above it, so the host's unhandled-error reporter sees it — matching the legacy plugin path's `runEventsInBatch` + `rethrowCaughtError` behavior of propagating the first listener error after the dispatch batch completes. The dispatch loop itself continues normally so subsequent listeners (e.g. parent bubble handlers) still fire. Updates the corresponding test in `EventTargetDispatching-itest.js` to drop the `console.error` mock setup that was specific to the old behavior. Both code paths now satisfy the same assertion (`expect(dispatch).toThrow('handler error')`) — the legacy path throws synchronously after the React batch; the new path's async re-throw surfaces inside Fantom's internal work-loop pump during `Fantom.dispatchNativeEvent`. Changelog: [Internal] Reviewed By: javache Differential Revision: D104650049 fbshipit-source-id: 793072f82c9abf11f4fad23b3c1f044f0e5d2936
This commit is contained in:
committed by
meta-codesync[bot]
parent
37909420f3
commit
645f846c73
+1
-27
@@ -1322,21 +1322,6 @@ const {isOSS} = Fantom.getConstants();
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
let originalConsoleError: typeof console.error;
|
||||
let mockConsoleError: JestMockFn<$FlowFixMe, $FlowFixMe>;
|
||||
|
||||
beforeEach(() => {
|
||||
originalConsoleError = console.error;
|
||||
mockConsoleError = jest.fn();
|
||||
// $FlowFixMe[cannot-write]
|
||||
console.error = mockConsoleError;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// $FlowFixMe[cannot-write]
|
||||
console.error = originalConsoleError;
|
||||
});
|
||||
|
||||
it('error in event handler does not break dispatch to subsequent listeners', () => {
|
||||
const root = Fantom.createRoot();
|
||||
const childRef = React.createRef<React.ElementRef<typeof View>>();
|
||||
@@ -1365,18 +1350,7 @@ const {isOSS} = Fantom.getConstants();
|
||||
},
|
||||
);
|
||||
|
||||
if (ReactNativeFeatureFlags.enableNativeEventTargetEventDispatching()) {
|
||||
// EventTarget-style dispatch catches per-listener errors and
|
||||
// reports them via `console.error` (see `EventTarget.js`), so the
|
||||
// dispatch itself does not throw.
|
||||
dispatch();
|
||||
expect(mockConsoleError).toHaveBeenCalled();
|
||||
} else {
|
||||
// Legacy dispatch surfaces the first per-handler error via
|
||||
// Fantom's global handler, which re-throws synchronously after
|
||||
// dispatch completes.
|
||||
expect(dispatch).toThrow('handler error');
|
||||
}
|
||||
expect(dispatch).toThrow('handler error');
|
||||
|
||||
// The parent bubble handler should still fire despite child's error
|
||||
expect(parentHandler).toHaveBeenCalledTimes(1);
|
||||
|
||||
@@ -382,8 +382,7 @@ function invoke(
|
||||
try {
|
||||
propListener.call(eventTarget, event);
|
||||
} catch (error) {
|
||||
// TODO: replace with `reportError` when it's available.
|
||||
console.error(error);
|
||||
reportListenerError(error);
|
||||
}
|
||||
global.event = currentEvent;
|
||||
return;
|
||||
@@ -452,8 +451,7 @@ function invokeListeners(
|
||||
callback.handleEvent(event);
|
||||
}
|
||||
} catch (error) {
|
||||
// TODO: replace with `reportError` when it's available.
|
||||
console.error(error);
|
||||
reportListenerError(error);
|
||||
}
|
||||
|
||||
if (listener.passive) {
|
||||
@@ -507,3 +505,19 @@ function setEventDispatchFlag(event: Event, value: boolean): void {
|
||||
// $FlowExpectedError[prop-missing]
|
||||
event[EVENT_DISPATCH_FLAG] = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Surface a listener error to the global error handler without aborting the
|
||||
* rest of the dispatch. Throws in a new task so the error becomes an
|
||||
* uncaught exception (matching the legacy plugin path's behavior of
|
||||
* propagating listener errors via React's runEventsInBatch +
|
||||
* `rethrowCaughtError`, rather than swallowing them as a `console.error`).
|
||||
*
|
||||
* `setTimeout(0)` schedules a new macrotask; the throw inside it has no
|
||||
* catcher above, so it bubbles up to the host's unhandled-error reporter.
|
||||
*/
|
||||
function reportListenerError(error: unknown): void {
|
||||
setTimeout(() => {
|
||||
throw error;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user