diff --git a/packages/react-native/Libraries/Core/ErrorHandlers.js b/packages/react-native/Libraries/Core/ErrorHandlers.js new file mode 100644 index 00000000000000..036175bf9b823f --- /dev/null +++ b/packages/react-native/Libraries/Core/ErrorHandlers.js @@ -0,0 +1,116 @@ +/** + * 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. + * + * @format + * @flow strict + */ + +'use strict'; + +import type {ExtendedError} from './ExtendedError'; + +import {SyntheticError, handleException} from './ExceptionsManager'; + +type ErrorInfo = { + +componentStack?: ?string, + // $FlowFixMe[unclear-type] unknown props and state. + +errorBoundary?: ?React$Component, +}; + +export function onUncaughtError(errorValue: mixed, errorInfo: ErrorInfo): void { + let error; + + // Typically, `errorValue` should be an error. However, other values such as + // strings (or even null) are sometimes thrown. + if (errorValue instanceof Error) { + /* $FlowFixMe[class-object-subtyping] added when improving typing for + * this parameters */ + error = (errorValue: ExtendedError); + } else if (typeof errorValue === 'string') { + /* $FlowFixMe[class-object-subtyping] added when improving typing for + * this parameters */ + error = (new SyntheticError(errorValue): ExtendedError); + } else { + /* $FlowFixMe[class-object-subtyping] added when improving typing for + * this parameters */ + error = (new SyntheticError('Unspecified error'): ExtendedError); + } + try { + // $FlowFixMe[incompatible-use] this is in try/catch. + error.componentStack = errorInfo.componentStack; + error.isComponentError = true; + } catch { + // Ignored. + } + + // Uncaught errors are fatal. + handleException(error, true); +} + +export function onCaughtError(errorValue: mixed, errorInfo: ErrorInfo): void { + let error; + + // Typically, `errorValue` should be an error. However, other values such as + // strings (or even null) are sometimes thrown. + if (errorValue instanceof Error) { + /* $FlowFixMe[class-object-subtyping] added when improving typing for + * this parameters */ + error = (errorValue: ExtendedError); + } else if (typeof errorValue === 'string') { + /* $FlowFixMe[class-object-subtyping] added when improving typing for + * this parameters */ + error = (new SyntheticError(errorValue): ExtendedError); + } else { + /* $FlowFixMe[class-object-subtyping] added when improving typing for + * this parameters */ + error = (new SyntheticError('Unspecified error'): ExtendedError); + } + try { + // $FlowFixMe[incompatible-use] this is in try/catch. + error.componentStack = errorInfo.componentStack; + error.isComponentError = true; + } catch { + // Ignored. + } + + // Caught errors are not fatal. + handleException(error, false); +} + +export function onRecoverableError( + errorValue: mixed, + errorInfo: ErrorInfo, +): void { + let error; + + // Typically, `errorValue` should be an error. However, other values such as + // strings (or even null) are sometimes thrown. + if (errorValue instanceof Error) { + /* $FlowFixMe[class-object-subtyping] added when improving typing for + * this parameters */ + error = (errorValue: ExtendedError); + } else if (typeof errorValue === 'string') { + /* $FlowFixMe[class-object-subtyping] added when improving typing for + * this parameters */ + error = (new SyntheticError(errorValue): ExtendedError); + } else { + /* $FlowFixMe[class-object-subtyping] added when improving typing for + * this parameters */ + error = (new SyntheticError('Unspecified error'): ExtendedError); + } + try { + // $FlowFixMe[incompatible-use] this is in try/catch. + error.componentStack = errorInfo.componentStack; + error.isComponentError = true; + } catch { + // Ignored. + } + + // Recoverable errors should only be warnings. + // This will make it a soft error in LogBox. + // TODO: improve the logging for recoverable errors in prod. + console.warn(error); +} diff --git a/packages/react-native/Libraries/ReactNative/RendererImplementation.js b/packages/react-native/Libraries/ReactNative/RendererImplementation.js index c55f938a26d2e1..2ba6ef04c83e9a 100644 --- a/packages/react-native/Libraries/ReactNative/RendererImplementation.js +++ b/packages/react-native/Libraries/ReactNative/RendererImplementation.js @@ -12,8 +12,12 @@ import type {HostComponent} from '../Renderer/shims/ReactNativeTypes'; import type ReactFabricHostComponent from './ReactFabricPublicInstance/ReactFabricHostComponent'; import type {Element, ElementRef, ElementType} from 'react'; +import { + onCaughtError, + onRecoverableError, + onUncaughtError, +} from '../Core/ErrorHandlers'; import {type RootTag} from './RootTag'; - export function renderElement({ element, rootTag, @@ -31,9 +35,23 @@ export function renderElement({ rootTag, null, useConcurrentRoot, + { + onCaughtError, + onUncaughtError, + onRecoverableError, + }, ); } else { - require('../Renderer/shims/ReactNative').render(element, rootTag); + require('../Renderer/shims/ReactNative').render( + element, + rootTag, + undefined, + { + onCaughtError, + onUncaughtError, + onRecoverableError, + }, + ); } } diff --git a/packages/react-native/Libraries/Renderer/shims/ReactFabric.js b/packages/react-native/Libraries/Renderer/shims/ReactFabric.js index 96f4a83be29cef..879091ef5fb4a7 100644 --- a/packages/react-native/Libraries/Renderer/shims/ReactFabric.js +++ b/packages/react-native/Libraries/Renderer/shims/ReactFabric.js @@ -8,6 +8,9 @@ * @flow * @nolint * @generated SignedSource<> + * @generated SignedSource<<19f13c8d8dac82cd391ad408f5ad8893>> + * + * This file was sync'd from the facebook/react repository. */ 'use strict'; diff --git a/packages/react-native/Libraries/Renderer/shims/ReactFeatureFlags.js b/packages/react-native/Libraries/Renderer/shims/ReactFeatureFlags.js index d9d5a8b75ffbba..9bafcc90bc9cc4 100644 --- a/packages/react-native/Libraries/Renderer/shims/ReactFeatureFlags.js +++ b/packages/react-native/Libraries/Renderer/shims/ReactFeatureFlags.js @@ -8,6 +8,9 @@ * @flow strict-local * @nolint * @generated SignedSource<<2881c8e89ef0f73f4cf6612cb518b197>> + * @generated SignedSource<> + * + * This file was sync'd from the facebook/react repository. */ 'use strict'; diff --git a/packages/react-native/Libraries/Renderer/shims/ReactNative.js b/packages/react-native/Libraries/Renderer/shims/ReactNative.js index debaf0ae01cc34..07d66cfd21d71a 100644 --- a/packages/react-native/Libraries/Renderer/shims/ReactNative.js +++ b/packages/react-native/Libraries/Renderer/shims/ReactNative.js @@ -8,6 +8,9 @@ * @flow * @nolint * @generated SignedSource<<0debd6e5a17dc037cb4661315a886de6>> + * @generated SignedSource<<228cd610b28ff12c92264be0d9be9374>> + * + * This file was sync'd from the facebook/react repository. */ 'use strict'; diff --git a/packages/react-native/Libraries/Renderer/shims/ReactNativeTypes.js b/packages/react-native/Libraries/Renderer/shims/ReactNativeTypes.js index 65daba73e3a6a4..eaea39febbe1aa 100644 --- a/packages/react-native/Libraries/Renderer/shims/ReactNativeTypes.js +++ b/packages/react-native/Libraries/Renderer/shims/ReactNativeTypes.js @@ -7,7 +7,10 @@ * @noformat * @flow strict * @nolint - * @generated SignedSource<> + * @generated SignedSource<> + * @generated SignedSource<<39aa3fa373095c6b192fac24f5b6c30c>> + * + * This file was sync'd from the facebook/react repository. */ import type {ElementRef, ElementType, Element, AbstractComponent} from 'react'; @@ -176,6 +179,25 @@ export type TouchedViewDataAtPoint = $ReadOnly<{ ...InspectorData, }>; +export type RenderRootOptions = { + onUncaughtError?: ( + error: mixed, + errorInfo: {+componentStack?: ?string}, + ) => void, + onCaughtError?: ( + error: mixed, + errorInfo: { + +componentStack?: ?string, + // $FlowFixMe[unclear-type] unknown props and state. + +errorBoundary?: ?React$Component, + }, + ) => void, + onRecoverableError?: ( + error: mixed, + errorInfo: {+componentStack?: ?string}, + ) => void, +}; + /** * Flat ReactNative renderer bundles are too big for Flow to parse efficiently. * Provide minimal Flow typing for the high-level RN API and call it a day. @@ -204,6 +226,7 @@ export type ReactNativeType = { element: Element, containerTag: number, callback: ?() => void, + options: ?RenderRootOptions, ): ?ElementRef, unmountComponentAtNode(containerTag: number): void, unmountComponentAtNodeAndRemoveContainer(containerTag: number): void, @@ -239,6 +262,7 @@ export type ReactFabricType = { containerTag: number, callback: ?() => void, concurrentRoot: ?boolean, + options: ?RenderRootOptions, ): ?ElementRef, unmountComponentAtNode(containerTag: number): void, getNodeFromInternalInstanceHandle( diff --git a/packages/react-native/Libraries/Renderer/shims/ReactNativeViewConfigRegistry.js b/packages/react-native/Libraries/Renderer/shims/ReactNativeViewConfigRegistry.js index cbcdfcebfac713..4d87046170ccc6 100644 --- a/packages/react-native/Libraries/Renderer/shims/ReactNativeViewConfigRegistry.js +++ b/packages/react-native/Libraries/Renderer/shims/ReactNativeViewConfigRegistry.js @@ -8,6 +8,9 @@ * @flow strict-local * @nolint * @generated SignedSource<<73af5b3fe29d226634ed64bc861634df>> + * @generated SignedSource<> + * + * This file was sync'd from the facebook/react repository. */ 'use strict'; diff --git a/packages/react-native/Libraries/Renderer/shims/createReactNativeComponentClass.js b/packages/react-native/Libraries/Renderer/shims/createReactNativeComponentClass.js index a4b177e0ea6839..e6e403c751e72f 100644 --- a/packages/react-native/Libraries/Renderer/shims/createReactNativeComponentClass.js +++ b/packages/react-native/Libraries/Renderer/shims/createReactNativeComponentClass.js @@ -8,6 +8,9 @@ * @flow strict-local * @nolint * @generated SignedSource<> + * @generated SignedSource<> + * + * This file was sync'd from the facebook/react repository. */ 'use strict'; diff --git a/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap b/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap index 2894c7a2ff7303..54f4271db5d5a7 100644 --- a/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap +++ b/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap @@ -3889,6 +3889,26 @@ declare module.exports: symbolicateStackTrace; " `; +exports[`public API should not change unintentionally Libraries/Core/ErrorHandlers.js 1`] = ` +"type ErrorInfo = { + +componentStack?: ?string, + +errorBoundary?: ?React$Component, +}; +declare export function onUncaughtError( + errorValue: mixed, + errorInfo: ErrorInfo +): void; +declare export function onCaughtError( + errorValue: mixed, + errorInfo: ErrorInfo +): void; +declare export function onRecoverableError( + errorValue: mixed, + errorInfo: ErrorInfo +): void; +" +`; + exports[`public API should not change unintentionally Libraries/Core/ExceptionsManager.js 1`] = ` "declare class SyntheticError extends Error { name: string; @@ -7121,6 +7141,23 @@ export type TouchedViewDataAtPoint = $ReadOnly<{ }>, ...InspectorData, }>; +export type RenderRootOptions = { + onUncaughtError?: ( + error: mixed, + errorInfo: { +componentStack?: ?string } + ) => void, + onCaughtError?: ( + error: mixed, + errorInfo: { + +componentStack?: ?string, + +errorBoundary?: ?React$Component, + } + ) => void, + onRecoverableError?: ( + error: mixed, + errorInfo: { +componentStack?: ?string } + ) => void, +}; export type ReactNativeType = { findHostInstance_DEPRECATED( componentOrHandle: ?(ElementRef | number) @@ -7144,7 +7181,8 @@ export type ReactNativeType = { render( element: Element, containerTag: number, - callback: ?() => void + callback: ?() => void, + options: ?RenderRootOptions ): ?ElementRef, unmountComponentAtNode(containerTag: number): void, unmountComponentAtNodeAndRemoveContainer(containerTag: number): void, @@ -7177,7 +7215,8 @@ export type ReactFabricType = { element: Element, containerTag: number, callback: ?() => void, - concurrentRoot: ?boolean + concurrentRoot: ?boolean, + options: ?RenderRootOptions ): ?ElementRef, unmountComponentAtNode(containerTag: number): void, getNodeFromInternalInstanceHandle(