@samitouri / QOS-React-2 / commits / a053716077

Make onUncaughtError and onCaughtError Configurable (#28641)

Stacked on #28627. This makes error logging configurable using these `createRoot`/`hydrateRoot` options: ``` onUncaughtError(error: mixed, errorInfo: {componentStack?: ?string}) => void onCaughtError(error: mixed, errorInfo: {componentStack?: ?string, errorBoundary?: ?React.Component<any, any>}) => void onRecoverableError(error: mixed, errorInfo: {digest?: ?string, componentStack?: ?string}) => void ``` We already have the `onRecoverableError` option since before. Overriding these can be used to implement custom error dialogs (with access to the `componentStack`). It can also be used to silence caught errors when testing an error boundary or if you prefer not getting logs for caught errors that you've already handled in an error boundary. I currently expose the error boundary instance but I think we should probably remove that since it doesn't make sense for non-class error boundaries and isn't very useful anyway. It's also unclear what it should do when an error is rethrown from one boundary to another. Since these are public APIs now we can implement the ReactFiberErrorDialog forks using these options at the roots of the builds. So I unforked those files and instead passed a custom option for the native and www builds. To do this I had to fork the ReactDOMLegacy file into ReactDOMRootFB which is a duplication but that will go away as soon as the FB fork is the only legacy root.

Sebastian Markbåge committed Mar 26, 2024 at 21:51 UTC a0537160771bafae90c6fd3154eeead2f2c903e7
25 files changed +1115 -275
packages/react-dom/index.classic.fb.js
+2 -3
@@ -20,11 +20,8 @@ Object.assign((Internals: any), {
20
21 export {
22 createPortal,
23 - createRoot,
24 - hydrateRoot,
23 findDOMNode,
24 flushSync,
27 - render,
25 unmountComponentAtNode,
26 unstable_batchedUpdates,
27 unstable_createEventHandle,
@@ -41,4 +38,6 @@ export {
38 version,
39 } from './src/client/ReactDOM';
40
41 +export {createRoot, hydrateRoot, render} from './src/client/ReactDOMRootFB';
42 +
43 export {Internals as __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED};
packages/react-dom/index.modern.fb.js
+2 -2
@@ -10,8 +10,6 @@
10 export {default as __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED} from './src/ReactDOMSharedInternals';
11 export {
12 createPortal,
13 - createRoot,
14 - hydrateRoot,
13 flushSync,
14 unstable_batchedUpdates,
15 unstable_createEventHandle,
@@ -26,3 +24,5 @@ export {
24 preinitModule,
25 version,
26 } from './src/client/ReactDOM';
27 +
28 +export {createRoot, hydrateRoot} from './src/client/ReactDOMRootFB';
packages/react-dom/src/__tests__/ReactDOMRoot-test.js
+2
@@ -47,6 +47,7 @@ describe('ReactDOMRoot', () => {
47 expect(container.textContent).toEqual('Hi');
48 });
49
50 + // @gate !classic || !__DEV__
51 it('warns if you import createRoot from react-dom', async () => {
52 expect(() => ReactDOM.createRoot(container)).toErrorDev(
53 'You are importing createRoot from "react-dom" which is not supported. ' +
@@ -57,6 +58,7 @@ describe('ReactDOMRoot', () => {
58 );
59 });
60
61 + // @gate !classic || !__DEV__
62 it('warns if you import hydrateRoot from react-dom', async () => {
63 expect(() => ReactDOM.hydrateRoot(container, null)).toErrorDev(
64 'You are importing hydrateRoot from "react-dom" which is not supported. ' +
packages/react-dom/src/client/ReactDOMLegacy.js
+7 -1
@@ -39,6 +39,8 @@ import {
39 getPublicRootInstance,
40 findHostInstance,
41 findHostInstanceWithWarning,
42 + defaultOnUncaughtError,
43 + defaultOnCaughtError,
44 } from 'react-reconciler/src/ReactFiberReconciler';
45 import {LegacyRoot} from 'react-reconciler/src/ReactRootTags';
46 import getComponentNameFromType from 'shared/getComponentNameFromType';
@@ -124,6 +126,8 @@ function legacyCreateRootFromDOMContainer(
126 false, // isStrictMode
127 false, // concurrentUpdatesByDefaultOverride,
128 '', // identifierPrefix
129 + defaultOnUncaughtError,
130 + defaultOnCaughtError,
131 noopOnRecoverableError,
132 // TODO(luna) Support hydration later
133 null,
@@ -158,7 +162,9 @@ function legacyCreateRootFromDOMContainer(
162 false, // isStrictMode
163 false, // concurrentUpdatesByDefaultOverride,
164 '', // identifierPrefix
161 - noopOnRecoverableError, // onRecoverableError
165 + defaultOnUncaughtError,
166 + defaultOnCaughtError,
167 + noopOnRecoverableError,
168 null, // transitionCallbacks
169 );
170 container._reactRootContainer = root;
packages/react-dom/src/client/ReactDOMRoot.js
+53 -8
@@ -32,7 +32,21 @@ export type CreateRootOptions = {
32 unstable_concurrentUpdatesByDefault?: boolean,
33 unstable_transitionCallbacks?: TransitionTracingCallbacks,
34 identifierPrefix?: string,
35 - onRecoverableError?: (error: mixed) => void,
35 + onUncaughtError?: (
36 + error: mixed,
37 + errorInfo: {+componentStack?: ?string},
38 + ) => void,
39 + onCaughtError?: (
40 + error: mixed,
41 + errorInfo: {
42 + +componentStack?: ?string,
43 + +errorBoundary?: ?React$Component<any, any>,
44 + },
45 + ) => void,
46 + onRecoverableError?: (
47 + error: mixed,
48 + errorInfo: {+digest?: ?string, +componentStack?: ?string},
49 + ) => void,
50 };
51
52 export type HydrateRootOptions = {
@@ -44,7 +58,21 @@ export type HydrateRootOptions = {
58 unstable_concurrentUpdatesByDefault?: boolean,
59 unstable_transitionCallbacks?: TransitionTracingCallbacks,
60 identifierPrefix?: string,
47 - onRecoverableError?: (error: mixed) => void,
61 + onUncaughtError?: (
62 + error: mixed,
63 + errorInfo: {+componentStack?: ?string},
64 + ) => void,
65 + onCaughtError?: (
66 + error: mixed,
67 + errorInfo: {
68 + +componentStack?: ?string,
69 + +errorBoundary?: ?React$Component<any, any>,
70 + },
71 + ) => void,
72 + onRecoverableError?: (
73 + error: mixed,
74 + errorInfo: {+digest?: ?string, +componentStack?: ?string},
75 + ) => void,
76 formState?: ReactFormState<any, any> | null,
77 };
78
@@ -67,15 +95,12 @@ import {
95 updateContainer,
96 flushSync,
97 isAlreadyRendering,
98 + defaultOnUncaughtError,
99 + defaultOnCaughtError,
100 + defaultOnRecoverableError,
101 } from 'react-reconciler/src/ReactFiberReconciler';
102 import {ConcurrentRoot} from 'react-reconciler/src/ReactRootTags';
103
73 -import reportGlobalError from 'shared/reportGlobalError';
74 -
75 -function defaultOnRecoverableError(error: mixed, errorInfo: any) {
76 - reportGlobalError(error);
77 -}
78 -
104 // $FlowFixMe[missing-this-annot]
105 function ReactDOMRoot(internalRoot: FiberRoot) {
106 this._internalRoot = internalRoot;
@@ -156,6 +181,8 @@ export function createRoot(
181 let isStrictMode = false;
182 let concurrentUpdatesByDefaultOverride = false;
183 let identifierPrefix = '';
184 + let onUncaughtError = defaultOnUncaughtError;
185 + let onCaughtError = defaultOnCaughtError;
186 let onRecoverableError = defaultOnRecoverableError;
187 let transitionCallbacks = null;
188
@@ -193,6 +220,12 @@ export function createRoot(
220 if (options.identifierPrefix !== undefined) {
221 identifierPrefix = options.identifierPrefix;
222 }
223 + if (options.onUncaughtError !== undefined) {
224 + onUncaughtError = options.onUncaughtError;
225 + }
226 + if (options.onCaughtError !== undefined) {
227 + onCaughtError = options.onCaughtError;
228 + }
229 if (options.onRecoverableError !== undefined) {
230 onRecoverableError = options.onRecoverableError;
231 }
@@ -208,6 +241,8 @@ export function createRoot(
241 isStrictMode,
242 concurrentUpdatesByDefaultOverride,
243 identifierPrefix,
244 + onUncaughtError,
245 + onCaughtError,
246 onRecoverableError,
247 transitionCallbacks,
248 );
@@ -262,6 +297,8 @@ export function hydrateRoot(
297 let isStrictMode = false;
298 let concurrentUpdatesByDefaultOverride = false;
299 let identifierPrefix = '';
300 + let onUncaughtError = defaultOnUncaughtError;
301 + let onCaughtError = defaultOnCaughtError;
302 let onRecoverableError = defaultOnRecoverableError;
303 let transitionCallbacks = null;
304 let formState = null;
@@ -278,6 +315,12 @@ export function hydrateRoot(
315 if (options.identifierPrefix !== undefined) {
316 identifierPrefix = options.identifierPrefix;
317 }
318 + if (options.onUncaughtError !== undefined) {
319 + onUncaughtError = options.onUncaughtError;
320 + }
321 + if (options.onCaughtError !== undefined) {
322 + onCaughtError = options.onCaughtError;
323 + }
324 if (options.onRecoverableError !== undefined) {
325 onRecoverableError = options.onRecoverableError;
326 }
@@ -300,6 +343,8 @@ export function hydrateRoot(
343 isStrictMode,
344 concurrentUpdatesByDefaultOverride,
345 identifierPrefix,
346 + onUncaughtError,
347 + onCaughtError,
348 onRecoverableError,
349 transitionCallbacks,
350 formState,
packages/react-dom/src/client/ReactDOMRootFB.js new
+418
@@ -0,0 +1,418 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +import type {ReactNodeList} from 'shared/ReactTypes';
11 +
12 +import type {
13 + RootType,
14 + CreateRootOptions,
15 + HydrateRootOptions,
16 +} from './ReactDOMRoot';
17 +
18 +import type {FiberRoot} from 'react-reconciler/src/ReactInternalTypes';
19 +
20 +import type {
21 + Container,
22 + PublicInstance,
23 +} from 'react-dom-bindings/src/client/ReactFiberConfigDOM';
24 +
25 +import {
26 + createRoot as createRootImpl,
27 + hydrateRoot as hydrateRootImpl,
28 +} from './ReactDOMRoot';
29 +
30 +import {disableLegacyMode} from 'shared/ReactFeatureFlags';
31 +import {clearContainer} from 'react-dom-bindings/src/client/ReactFiberConfigDOM';
32 +import {
33 + getInstanceFromNode,
34 + isContainerMarkedAsRoot,
35 + markContainerAsRoot,
36 +} from 'react-dom-bindings/src/client/ReactDOMComponentTree';
37 +import {listenToAllSupportedEvents} from 'react-dom-bindings/src/events/DOMPluginEventSystem';
38 +import {isValidContainerLegacy} from './ReactDOMRoot';
39 +import {
40 + DOCUMENT_NODE,
41 + COMMENT_NODE,
42 +} from 'react-dom-bindings/src/client/HTMLNodeType';
43 +
44 +import {
45 + createContainer,
46 + createHydrationContainer,
47 + findHostInstanceWithNoPortals,
48 + updateContainer,
49 + flushSync,
50 + getPublicRootInstance,
51 + defaultOnUncaughtError,
52 + defaultOnCaughtError,
53 +} from 'react-reconciler/src/ReactFiberReconciler';
54 +import {LegacyRoot} from 'react-reconciler/src/ReactRootTags';
55 +import {has as hasInstance} from 'shared/ReactInstanceMap';
56 +
57 +import assign from 'shared/assign';
58 +
59 +// Provided by www
60 +const ReactFiberErrorDialogWWW = require('ReactFiberErrorDialog');
61 +
62 +if (typeof ReactFiberErrorDialogWWW.showErrorDialog !== 'function') {
63 + throw new Error(
64 + 'Expected ReactFiberErrorDialog.showErrorDialog to be a function.',
65 + );
66 +}
67 +
68 +function wwwOnUncaughtError(
69 + error: mixed,
70 + errorInfo: {+componentStack?: ?string},
71 +): void {
72 + const componentStack =
73 + errorInfo.componentStack != null ? errorInfo.componentStack : '';
74 + const logError = ReactFiberErrorDialogWWW.showErrorDialog({
75 + errorBoundary: null,
76 + error,
77 + componentStack,
78 + });
79 +
80 + // Allow injected showErrorDialog() to prevent default console.error logging.
81 + // This enables renderers like ReactNative to better manage redbox behavior.
82 + if (logError === false) {
83 + return;
84 + }
85 +
86 + defaultOnUncaughtError(error, errorInfo);
87 +}
88 +
89 +function wwwOnCaughtError(
90 + error: mixed,
91 + errorInfo: {
92 + +componentStack?: ?string,
93 + +errorBoundary?: ?React$Component<any, any>,
94 + },
95 +): void {
96 + const errorBoundary = errorInfo.errorBoundary;
97 + const componentStack =
98 + errorInfo.componentStack != null ? errorInfo.componentStack : '';
99 + const logError = ReactFiberErrorDialogWWW.showErrorDialog({
100 + errorBoundary,
101 + error,
102 + componentStack,
103 + });
104 +
105 + // Allow injected showErrorDialog() to prevent default console.error logging.
106 + // This enables renderers like ReactNative to better manage redbox behavior.
107 + if (logError === false) {
108 + return;
109 + }
110 +
111 + defaultOnCaughtError(error, errorInfo);
112 +}
113 +
114 +export function createRoot(
115 + container: Element | Document | DocumentFragment,
116 + options?: CreateRootOptions,
117 +): RootType {
118 + return createRootImpl(
119 + container,
120 + assign(
121 + ({
122 + onUncaughtError: wwwOnUncaughtError,
123 + onCaughtError: wwwOnCaughtError,
124 + }: any),
125 + options,
126 + ),
127 + );
128 +}
129 +
130 +export function hydrateRoot(
131 + container: Document | Element,
132 + initialChildren: ReactNodeList,
133 + options?: HydrateRootOptions,
134 +): RootType {
135 + return hydrateRootImpl(
136 + container,
137 + initialChildren,
138 + assign(
139 + ({
140 + onUncaughtError: wwwOnUncaughtError,
141 + onCaughtError: wwwOnCaughtError,
142 + }: any),
143 + options,
144 + ),
145 + );
146 +}
147 +
148 +let topLevelUpdateWarnings;
149 +
150 +if (__DEV__) {
151 + topLevelUpdateWarnings = (container: Container) => {
152 + if (container._reactRootContainer && container.nodeType !== COMMENT_NODE) {
153 + const hostInstance = findHostInstanceWithNoPortals(
154 + container._reactRootContainer.current,
155 + );
156 + if (hostInstance) {
157 + if (hostInstance.parentNode !== container) {
158 + console.error(
159 + 'It looks like the React-rendered content of this ' +
160 + 'container was removed without using React. This is not ' +
161 + 'supported and will cause errors. Instead, call ' +
162 + 'ReactDOM.unmountComponentAtNode to empty a container.',
163 + );
164 + }
165 + }
166 + }
167 +
168 + const isRootRenderedBySomeReact = !!container._reactRootContainer;
169 + const rootEl = getReactRootElementInContainer(container);
170 + const hasNonRootReactChild = !!(rootEl && getInstanceFromNode(rootEl));
171 +
172 + if (hasNonRootReactChild && !isRootRenderedBySomeReact) {
173 + console.error(
174 + 'Replacing React-rendered children with a new root ' +
175 + 'component. If you intended to update the children of this node, ' +
176 + 'you should instead have the existing children update their state ' +
177 + 'and render the new components instead of calling ReactDOM.render.',
178 + );
179 + }
180 + };
181 +}
182 +
183 +function getReactRootElementInContainer(container: any) {
184 + if (!container) {
185 + return null;
186 + }
187 +
188 + if (container.nodeType === DOCUMENT_NODE) {
189 + return container.documentElement;
190 + } else {
191 + return container.firstChild;
192 + }
193 +}
194 +
195 +function noopOnRecoverableError() {
196 + // This isn't reachable because onRecoverableError isn't called in the
197 + // legacy API.
198 +}
199 +
200 +function legacyCreateRootFromDOMContainer(
201 + container: Container,
202 + initialChildren: ReactNodeList,
203 + parentComponent: ?React$Component<any, any>,
204 + callback: ?Function,
205 + isHydrationContainer: boolean,
206 +): FiberRoot {
207 + if (isHydrationContainer) {
208 + if (typeof callback === 'function') {
209 + const originalCallback = callback;
210 + callback = function () {
211 + const instance = getPublicRootInstance(root);
212 + originalCallback.call(instance);
213 + };
214 + }
215 +
216 + const root: FiberRoot = createHydrationContainer(
217 + initialChildren,
218 + callback,
219 + container,
220 + LegacyRoot,
221 + null, // hydrationCallbacks
222 + false, // isStrictMode
223 + false, // concurrentUpdatesByDefaultOverride,
224 + '', // identifierPrefix
225 + wwwOnUncaughtError,
226 + wwwOnCaughtError,
227 + noopOnRecoverableError,
228 + // TODO(luna) Support hydration later
229 + null,
230 + null,
231 + );
232 + container._reactRootContainer = root;
233 + markContainerAsRoot(root.current, container);
234 +
235 + const rootContainerElement =
236 + container.nodeType === COMMENT_NODE ? container.parentNode : container;
237 + // $FlowFixMe[incompatible-call]
238 + listenToAllSupportedEvents(rootContainerElement);
239 +
240 + flushSync();
241 + return root;
242 + } else {
243 + // First clear any existing content.
244 + clearContainer(container);
245 +
246 + if (typeof callback === 'function') {
247 + const originalCallback = callback;
248 + callback = function () {
249 + const instance = getPublicRootInstance(root);
250 + originalCallback.call(instance);
251 + };
252 + }
253 +
254 + const root = createContainer(
255 + container,
256 + LegacyRoot,
257 + null, // hydrationCallbacks
258 + false, // isStrictMode
259 + false, // concurrentUpdatesByDefaultOverride,
260 + '', // identifierPrefix
261 + wwwOnUncaughtError,
262 + wwwOnCaughtError,
263 + noopOnRecoverableError,
264 + null, // transitionCallbacks
265 + );
266 + container._reactRootContainer = root;
267 + markContainerAsRoot(root.current, container);
268 +
269 + const rootContainerElement =
270 + container.nodeType === COMMENT_NODE ? container.parentNode : container;
271 + // $FlowFixMe[incompatible-call]
272 + listenToAllSupportedEvents(rootContainerElement);
273 +
274 + // Initial mount should not be batched.
275 + flushSync(() => {
276 + updateContainer(initialChildren, root, parentComponent, callback);
277 + });
278 +
279 + return root;
280 + }
281 +}
282 +
283 +function warnOnInvalidCallback(callback: mixed): void {
284 + if (__DEV__) {
285 + if (callback !== null && typeof callback !== 'function') {
286 + console.error(
287 + 'Expected the last optional `callback` argument to be a ' +
288 + 'function. Instead received: %s.',
289 + callback,
290 + );
291 + }
292 + }
293 +}
294 +
295 +function legacyRenderSubtreeIntoContainer(
296 + parentComponent: ?React$Component<any, any>,
297 + children: ReactNodeList,
298 + container: Container,
299 + forceHydrate: boolean,
300 + callback: ?Function,
301 +): React$Component<any, any> | PublicInstance | null {
302 + if (__DEV__) {
303 + topLevelUpdateWarnings(container);
304 + warnOnInvalidCallback(callback === undefined ? null : callback);
305 + }
306 +
307 + const maybeRoot = container._reactRootContainer;
308 + let root: FiberRoot;
309 + if (!maybeRoot) {
310 + // Initial mount
311 + root = legacyCreateRootFromDOMContainer(
312 + container,
313 + children,
314 + parentComponent,
315 + callback,
316 + forceHydrate,
317 + );
318 + } else {
319 + root = maybeRoot;
320 + if (typeof callback === 'function') {
321 + const originalCallback = callback;
322 + callback = function () {
323 + const instance = getPublicRootInstance(root);
324 + originalCallback.call(instance);
325 + };
326 + }
327 + // Update
328 + updateContainer(children, root, parentComponent, callback);
329 + }
330 + return getPublicRootInstance(root);
331 +}
332 +
333 +export function render(
334 + element: React$Element<any>,
335 + container: Container,
336 + callback: ?Function,
337 +): React$Component<any, any> | PublicInstance | null {
338 + if (disableLegacyMode) {
339 + if (__DEV__) {
340 + console.error(
341 + 'ReactDOM.render was removed in React 19. Use createRoot instead.',
342 + );
343 + }
344 + throw new Error('ReactDOM: Unsupported Legacy Mode API.');
345 + }
346 + if (__DEV__) {
347 + console.error(
348 + 'ReactDOM.render has not been supported since React 18. Use createRoot ' +
349 + 'instead. Until you switch to the new API, your app will behave as ' +
350 + "if it's running React 17. Learn " +
351 + 'more: https://react.dev/link/switch-to-createroot',
352 + );
353 + }
354 +
355 + if (!isValidContainerLegacy(container)) {
356 + throw new Error('Target container is not a DOM element.');
357 + }
358 +
359 + if (__DEV__) {
360 + const isModernRoot =
361 + isContainerMarkedAsRoot(container) &&
362 + container._reactRootContainer === undefined;
363 + if (isModernRoot) {
364 + console.error(
365 + 'You are calling ReactDOM.render() on a container that was previously ' +
366 + 'passed to ReactDOMClient.createRoot(). This is not supported. ' +
367 + 'Did you mean to call root.render(element)?',
368 + );
369 + }
370 + }
371 + return legacyRenderSubtreeIntoContainer(
372 + null,
373 + element,
374 + container,
375 + false,
376 + callback,
377 + );
378 +}
379 +
380 +export function unstable_renderSubtreeIntoContainer(
381 + parentComponent: React$Component<any, any>,
382 + element: React$Element<any>,
383 + containerNode: Container,
384 + callback: ?Function,
385 +): React$Component<any, any> | PublicInstance | null {
386 + if (disableLegacyMode) {
387 + if (__DEV__) {
388 + console.error(
389 + 'ReactDOM.unstable_renderSubtreeIntoContainer() was removed in React 19. Consider using a portal instead.',
390 + );
391 + }
392 + throw new Error('ReactDOM: Unsupported Legacy Mode API.');
393 + }
394 + if (__DEV__) {
395 + console.error(
396 + 'ReactDOM.unstable_renderSubtreeIntoContainer() has not been supported ' +
397 + 'since React 18. Consider using a portal instead. Until you switch to ' +
398 + "the createRoot API, your app will behave as if it's running React " +
399 + '17. Learn more: https://react.dev/link/switch-to-createroot',
400 + );
401 + }
402 +
403 + if (!isValidContainerLegacy(containerNode)) {
404 + throw new Error('Target container is not a DOM element.');
405 + }
406 +
407 + if (parentComponent == null || !hasInstance(parentComponent)) {
408 + throw new Error('parentComponent must be a valid React Component');
409 + }
410 +
411 + return legacyRenderSubtreeIntoContainer(
412 + parentComponent,
413 + element,
414 + containerNode,
415 + false,
416 + callback,
417 + );
418 +}
packages/react-dom/src/client/__mocks__/ReactFiberErrorDialog.js new
+12
@@ -0,0 +1,12 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +export function showErrorDialog(): boolean {
11 + return true;
12 +}
packages/react-native-renderer/src/ReactFabric.js
+58 -6
@@ -20,6 +20,9 @@ import {
20 updateContainer,
21 injectIntoDevTools,
22 getPublicRootInstance,
23 + defaultOnUncaughtError,
24 + defaultOnCaughtError,
25 + defaultOnRecoverableError,
26 } from 'react-reconciler/src/ReactFiberReconciler';
27
28 import {createPortal as createPortalImpl} from 'react-reconciler/src/ReactPortal';
@@ -43,11 +46,58 @@ import {
46 } from './ReactNativePublicCompat';
47 import {getPublicInstanceFromInternalInstanceHandle} from './ReactFiberConfigFabric';
48
46 -// $FlowFixMe[missing-local-annot]
47 -function onRecoverableError(error) {
48 - // TODO: Expose onRecoverableError option to userspace
49 - // eslint-disable-next-line react-internal/no-production-logging, react-internal/warning-args
50 - console.error(error);
49 +// Module provided by RN:
50 +import {ReactFiberErrorDialog} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface';
51 +
52 +if (typeof ReactFiberErrorDialog.showErrorDialog !== 'function') {
53 + throw new Error(
54 + 'Expected ReactFiberErrorDialog.showErrorDialog to be a function.',
55 + );
56 +}
57 +
58 +function nativeOnUncaughtError(
59 + error: mixed,
60 + errorInfo: {+componentStack?: ?string},
61 +): void {
62 + const componentStack =
63 + errorInfo.componentStack != null ? errorInfo.componentStack : '';
64 + const logError = ReactFiberErrorDialog.showErrorDialog({
65 + errorBoundary: null,
66 + error,
67 + componentStack,
68 + });
69 +
70 + // Allow injected showErrorDialog() to prevent default console.error logging.
71 + // This enables renderers like ReactNative to better manage redbox behavior.
72 + if (logError === false) {
73 + return;
74 + }
75 +
76 + defaultOnUncaughtError(error, errorInfo);
77 +}
78 +function nativeOnCaughtError(
79 + error: mixed,
80 + errorInfo: {
81 + +componentStack?: ?string,
82 + +errorBoundary?: ?React$Component<any, any>,
83 + },
84 +): void {
85 + const errorBoundary = errorInfo.errorBoundary;
86 + const componentStack =
87 + errorInfo.componentStack != null ? errorInfo.componentStack : '';
88 + const logError = ReactFiberErrorDialog.showErrorDialog({
89 + errorBoundary,
90 + error,
91 + componentStack,
92 + });
93 +
94 + // Allow injected showErrorDialog() to prevent default console.error logging.
95 + // This enables renderers like ReactNative to better manage redbox behavior.
96 + if (logError === false) {
97 + return;
98 + }
99 +
100 + defaultOnCaughtError(error, errorInfo);
101 }
102
103 function render(
@@ -68,7 +118,9 @@ function render(
118 false,
119 null,
120 '',
71 - onRecoverableError,
121 + nativeOnUncaughtError,
122 + nativeOnCaughtError,
123 + defaultOnRecoverableError,
124 null,
125 );
126 roots.set(containerTag, root);
packages/react-native-renderer/src/ReactNativeRenderer.js
+58 -6
@@ -20,6 +20,9 @@ import {
20 updateContainer,
21 injectIntoDevTools,
22 getPublicRootInstance,
23 + defaultOnUncaughtError,
24 + defaultOnCaughtError,
25 + defaultOnRecoverableError,
26 } from 'react-reconciler/src/ReactFiberReconciler';
27 // TODO: direct imports like some-package/src/* are bad. Fix me.
28 import {getStackByFiberInDevAndProd} from 'react-reconciler/src/ReactFiberComponentStack';
@@ -47,11 +50,58 @@ import {
50 isChildPublicInstance,
51 } from './ReactNativePublicCompat';
52
50 -// $FlowFixMe[missing-local-annot]
51 -function onRecoverableError(error) {
52 - // TODO: Expose onRecoverableError option to userspace
53 - // eslint-disable-next-line react-internal/no-production-logging, react-internal/warning-args
54 - console.error(error);
53 +// Module provided by RN:
54 +import {ReactFiberErrorDialog} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface';
55 +
56 +if (typeof ReactFiberErrorDialog.showErrorDialog !== 'function') {
57 + throw new Error(
58 + 'Expected ReactFiberErrorDialog.showErrorDialog to be a function.',
59 + );
60 +}
61 +
62 +function nativeOnUncaughtError(
63 + error: mixed,
64 + errorInfo: {+componentStack?: ?string},
65 +): void {
66 + const componentStack =
67 + errorInfo.componentStack != null ? errorInfo.componentStack : '';
68 + const logError = ReactFiberErrorDialog.showErrorDialog({
69 + errorBoundary: null,
70 + error,
71 + componentStack,
72 + });
73 +
74 + // Allow injected showErrorDialog() to prevent default console.error logging.
75 + // This enables renderers like ReactNative to better manage redbox behavior.
76 + if (logError === false) {
77 + return;
78 + }
79 +
80 + defaultOnUncaughtError(error, errorInfo);
81 +}
82 +function nativeOnCaughtError(
83 + error: mixed,
84 + errorInfo: {
85 + +componentStack?: ?string,
86 + +errorBoundary?: ?React$Component<any, any>,
87 + },
88 +): void {
89 + const errorBoundary = errorInfo.errorBoundary;
90 + const componentStack =
91 + errorInfo.componentStack != null ? errorInfo.componentStack : '';
92 + const logError = ReactFiberErrorDialog.showErrorDialog({
93 + errorBoundary,
94 + error,
95 + componentStack,
96 + });
97 +
98 + // Allow injected showErrorDialog() to prevent default console.error logging.
99 + // This enables renderers like ReactNative to better manage redbox behavior.
100 + if (logError === false) {
101 + return;
102 + }
103 +
104 + defaultOnCaughtError(error, errorInfo);
105 }
106
107 function render(
@@ -71,7 +121,9 @@ function render(
121 false,
122 null,
123 '',
74 - onRecoverableError,
124 + nativeOnUncaughtError,
125 + nativeOnCaughtError,
126 + defaultOnRecoverableError,
127 null,
128 );
129 roots.set(containerTag, root);
packages/react-noop-renderer/src/createReactNoop.js
+6
@@ -974,6 +974,8 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
974 null,
975 false,
976 '',
977 + NoopRenderer.defaultOnUncaughtError,
978 + NoopRenderer.defaultOnCaughtError,
979 onRecoverableError,
980 null,
981 );
@@ -996,6 +998,8 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
998 null,
999 false,
1000 '',
1001 + NoopRenderer.defaultOnUncaughtError,
1002 + NoopRenderer.defaultOnCaughtError,
1003 onRecoverableError,
1004 options && options.unstable_transitionCallbacks
1005 ? options.unstable_transitionCallbacks
@@ -1028,6 +1032,8 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
1032 null,
1033 false,
1034 '',
1035 + NoopRenderer.defaultOnUncaughtError,
1036 + NoopRenderer.defaultOnCaughtError,
1037 onRecoverableError,
1038 null,
1039 );
packages/react-reconciler/src/ReactFiberBeginWork.js
+14 -3
@@ -266,7 +266,10 @@ import {
266 createCapturedValueAtFiber,
267 type CapturedValue,
268 } from './ReactCapturedValue';
269 -import {createClassErrorUpdate} from './ReactFiberThrow';
269 +import {
270 + createClassErrorUpdate,
271 + initializeClassErrorUpdate,
272 +} from './ReactFiberThrow';
273 import is from 'shared/objectIs';
274 import {
275 getForksAtLevel,
@@ -1179,10 +1182,18 @@ function updateClassComponent(
1182 const lane = pickArbitraryLane(renderLanes);
1183 workInProgress.lanes = mergeLanes(workInProgress.lanes, lane);
1184 // Schedule the error boundary to re-render using updated state
1182 - const update = createClassErrorUpdate(
1185 + const root: FiberRoot | null = getWorkInProgressRoot();
1186 + if (root === null) {
1187 + throw new Error(
1188 + 'Expected a work-in-progress root. This is a bug in React. Please file an issue.',
1189 + );
1190 + }
1191 + const update = createClassErrorUpdate(lane);
1192 + initializeClassErrorUpdate(
1193 + update,
1194 + root,
1195 workInProgress,
1196 createCapturedValueAtFiber(error, workInProgress),
1185 - lane,
1197 );
1198 enqueueCapturedUpdate(workInProgress, update);
1199 break;
packages/react-reconciler/src/ReactFiberErrorDialog.js deleted
-22
@@ -1,22 +0,0 @@
1 -/**
2 - * Copyright (c) Meta Platforms, Inc. and affiliates.
3 - *
4 - * This source code is licensed under the MIT license found in the
5 - * LICENSE file in the root directory of this source tree.
6 - *
7 - * @flow
8 - */
9 -
10 -import type {Fiber} from './ReactInternalTypes';
11 -import type {CapturedValue} from './ReactCapturedValue';
12 -
13 -// This module is forked in different environments.
14 -// By default, return `true` to log errors to the console.
15 -// Forks can return `false` if this isn't desirable.
16 -
17 -export function showErrorDialog(
18 - boundary: Fiber,
19 - errorInfo: CapturedValue<mixed>,
20 -): boolean {
21 - return true;
22 -}
packages/react-reconciler/src/ReactFiberErrorLogger.js
+130 -77
@@ -7,99 +7,152 @@
7 * @flow
8 */
9
10 -import type {Fiber} from './ReactInternalTypes';
10 +import type {Fiber, FiberRoot} from './ReactInternalTypes';
11 import type {CapturedValue} from './ReactCapturedValue';
12
13 -import {showErrorDialog} from './ReactFiberErrorDialog';
13 import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber';
15 -import {HostRoot} from 'react-reconciler/src/ReactWorkTags';
14 +
15 +import {ClassComponent} from './ReactWorkTags';
16
17 import reportGlobalError from 'shared/reportGlobalError';
18
19 import ReactSharedInternals from 'shared/ReactSharedInternals';
20 const {ReactCurrentActQueue} = ReactSharedInternals;
21
22 -export function logCapturedError(
23 - boundary: Fiber,
22 +// Side-channel since I'm not sure we want to make this part of the public API
23 +let componentName: null | string = null;
24 +let errorBoundaryName: null | string = null;
25 +
26 +export function defaultOnUncaughtError(
27 + error: mixed,
28 + errorInfo: {+componentStack?: ?string},
29 +): void {
30 + // Overriding this can silence these warnings e.g. for tests.
31 + // See https://github.com/facebook/react/pull/13384
32 +
33 + // For uncaught root errors we report them as uncaught to the browser's
34 + // onerror callback. This won't have component stacks and the error addendum.
35 + // So we add those into a separate console.warn.
36 + reportGlobalError(error);
37 + if (__DEV__) {
38 + const componentStack =
39 + errorInfo.componentStack != null ? errorInfo.componentStack : '';
40 +
41 + const componentNameMessage = componentName
42 + ? `An error occurred in the <${componentName}> component:`
43 + : 'An error occurred in one of your React components:';
44 +
45 + console['warn'](
46 + '%s\n%s\n\n%s',
47 + componentNameMessage,
48 + componentStack || '',
49 + 'Consider adding an error boundary to your tree to customize error handling behavior.\n' +
50 + 'Visit https://react.dev/link/error-boundaries to learn more about error boundaries.',
51 + );
52 + }
53 +}
54 +
55 +export function defaultOnCaughtError(
56 + error: mixed,
57 + errorInfo: {
58 + +componentStack?: ?string,
59 + +errorBoundary?: ?React$Component<any, any>,
60 + },
61 +): void {
62 + // Overriding this can silence these warnings e.g. for tests.
63 + // See https://github.com/facebook/react/pull/13384
64 +
65 + // Caught by error boundary
66 + if (__DEV__) {
67 + const componentStack =
68 + errorInfo.componentStack != null ? errorInfo.componentStack : '';
69 +
70 + const componentNameMessage = componentName
71 + ? `The above error occurred in the <${componentName}> component:`
72 + : 'The above error occurred in one of your React components:';
73 +
74 + // In development, we provide our own message which includes the component stack
75 + // in addition to the error.
76 + // Don't transform to our wrapper
77 + console['error'](
78 + '%o\n\n%s\n%s\n\n%s',
79 + error,
80 + componentNameMessage,
81 + componentStack,
82 + `React will try to recreate this component tree from scratch ` +
83 + `using the error boundary you provided, ${
84 + errorBoundaryName || 'Anonymous'
85 + }.`,
86 + );
87 + } else {
88 + // In production, we print the error directly.
89 + // This will include the message, the JS stack, and anything the browser wants to show.
90 + // We pass the error object instead of custom message so that the browser displays the error natively.
91 + console['error'](error); // Don't transform to our wrapper
92 + }
93 +}
94 +
95 +export function defaultOnRecoverableError(
96 + error: mixed,
97 + errorInfo: {+digest?: ?string, +componentStack?: ?string},
98 +) {
99 + reportGlobalError(error);
100 +}
101 +
102 +export function logUncaughtError(
103 + root: FiberRoot,
104 errorInfo: CapturedValue<mixed>,
105 ): void {
106 try {
27 - const logError = showErrorDialog(boundary, errorInfo);
28 -
29 - // Allow injected showErrorDialog() to prevent default console.error logging.
30 - // This enables renderers like ReactNative to better manage redbox behavior.
31 - if (logError === false) {
32 - return;
107 + if (__DEV__) {
108 + componentName = errorInfo.source
109 + ? getComponentNameFromFiber(errorInfo.source)
110 + : null;
111 + errorBoundaryName = null;
112 }
34 -
113 const error = (errorInfo.value: any);
114 + if (__DEV__ && ReactCurrentActQueue.current !== null) {
115 + // For uncaught errors inside act, we track them on the act and then
116 + // rethrow them into the test.
117 + ReactCurrentActQueue.thrownErrors.push(error);
118 + return;
119 + }
120 + const onUncaughtError = root.onUncaughtError;
121 + onUncaughtError(error, {
122 + componentStack: errorInfo.stack,
123 + });
124 + } catch (e) {
125 + // This method must not throw, or React internal state will get messed up.
126 + // If console.error is overridden, or logCapturedError() shows a dialog that throws,
127 + // we want to report this error outside of the normal stack as a last resort.
128 + // https://github.com/facebook/react/issues/13188
129 + setTimeout(() => {
130 + throw e;
131 + });
132 + }
133 +}
134
37 - if (boundary.tag === HostRoot) {
38 - if (__DEV__ && ReactCurrentActQueue.current !== null) {
39 - // For uncaught errors inside act, we track them on the act and then
40 - // rethrow them into the test.
41 - ReactCurrentActQueue.thrownErrors.push(error);
42 - return;
43 - }
44 - // For uncaught root errors we report them as uncaught to the browser's
45 - // onerror callback. This won't have component stacks and the error addendum.
46 - // So we add those into a separate console.warn.
47 - reportGlobalError(error);
48 - if (__DEV__) {
49 - const source = errorInfo.source;
50 - const stack = errorInfo.stack;
51 - const componentStack = stack !== null ? stack : '';
52 - // TODO: There's no longer a way to silence these warnings e.g. for tests.
53 - // See https://github.com/facebook/react/pull/13384
54 -
55 - const componentName = source ? getComponentNameFromFiber(source) : null;
56 - const componentNameMessage = componentName
57 - ? `An error occurred in the <${componentName}> component:`
58 - : 'An error occurred in one of your React components:';
59 -
60 - console['warn'](
61 - '%s\n%s\n\n%s',
62 - componentNameMessage,
63 - componentStack,
64 - 'Consider adding an error boundary to your tree to customize error handling behavior.\n' +
65 - 'Visit https://react.dev/link/error-boundaries to learn more about error boundaries.',
66 - );
67 - }
68 - } else {
69 - // Caught by error boundary
70 - if (__DEV__) {
71 - const source = errorInfo.source;
72 - const stack = errorInfo.stack;
73 - const componentStack = stack !== null ? stack : '';
74 - // TODO: There's no longer a way to silence these warnings e.g. for tests.
75 - // See https://github.com/facebook/react/pull/13384
76 -
77 - const componentName = source ? getComponentNameFromFiber(source) : null;
78 - const componentNameMessage = componentName
79 - ? `The above error occurred in the <${componentName}> component:`
80 - : 'The above error occurred in one of your React components:';
81 -
82 - const errorBoundaryName =
83 - getComponentNameFromFiber(boundary) || 'Anonymous';
84 -
85 - // In development, we provide our own message which includes the component stack
86 - // in addition to the error.
87 - // Don't transform to our wrapper
88 - console['error'](
89 - '%o\n\n%s\n%s\n\n%s',
90 - error,
91 - componentNameMessage,
92 - componentStack,
93 - `React will try to recreate this component tree from scratch ` +
94 - `using the error boundary you provided, ${errorBoundaryName}.`,
95 - );
96 - } else {
97 - // In production, we print the error directly.
98 - // This will include the message, the JS stack, and anything the browser wants to show.
99 - // We pass the error object instead of custom message so that the browser displays the error natively.
100 - console['error'](error); // Don't transform to our wrapper
101 - }
135 +export function logCaughtError(
136 + root: FiberRoot,
137 + boundary: Fiber,
138 + errorInfo: CapturedValue<mixed>,
139 +): void {
140 + try {
141 + if (__DEV__) {
142 + componentName = errorInfo.source
143 + ? getComponentNameFromFiber(errorInfo.source)
144 + : null;
145 + errorBoundaryName = getComponentNameFromFiber(boundary);
146 }
147 + const error = (errorInfo.value: any);
148 + const onCaughtError = root.onCaughtError;
149 + onCaughtError(error, {
150 + componentStack: errorInfo.stack,
151 + errorBoundary:
152 + boundary.tag === ClassComponent
153 + ? boundary.stateNode // This should always be the case as long as we only have class boundaries
154 + : null,
155 + });
156 } catch (e) {
157 // This method must not throw, or React internal state will get messed up.
158 // If console.error is overridden, or logCapturedError() shows a dialog that throws,
packages/react-reconciler/src/ReactFiberReconciler.js
+39 -2
@@ -111,6 +111,11 @@ export {
111 observeVisibleRects,
112 } from './ReactTestSelectors';
113 export {startHostTransition} from './ReactFiberHooks';
114 +export {
115 + defaultOnUncaughtError,
116 + defaultOnCaughtError,
117 + defaultOnRecoverableError,
118 +} from './ReactFiberErrorLogger';
119
120 type OpaqueRoot = FiberRoot;
121
@@ -249,7 +254,21 @@ export function createContainer(
254 isStrictMode: boolean,
255 concurrentUpdatesByDefaultOverride: null | boolean,
256 identifierPrefix: string,
252 - onRecoverableError: (error: mixed) => void,
257 + onUncaughtError: (
258 + error: mixed,
259 + errorInfo: {+componentStack?: ?string},
260 + ) => void,
261 + onCaughtError: (
262 + error: mixed,
263 + errorInfo: {
264 + +componentStack?: ?string,
265 + +errorBoundary?: ?React$Component<any, any>,
266 + },
267 + ) => void,
268 + onRecoverableError: (
269 + error: mixed,
270 + errorInfo: {+digest?: ?string, +componentStack?: ?string},
271 + ) => void,
272 transitionCallbacks: null | TransitionTracingCallbacks,
273 ): OpaqueRoot {
274 const hydrate = false;
@@ -263,6 +282,8 @@ export function createContainer(
282 isStrictMode,
283 concurrentUpdatesByDefaultOverride,
284 identifierPrefix,
285 + onUncaughtError,
286 + onCaughtError,
287 onRecoverableError,
288 transitionCallbacks,
289 null,
@@ -279,7 +300,21 @@ export function createHydrationContainer(
300 isStrictMode: boolean,
301 concurrentUpdatesByDefaultOverride: null | boolean,
302 identifierPrefix: string,
282 - onRecoverableError: (error: mixed) => void,
303 + onUncaughtError: (
304 + error: mixed,
305 + errorInfo: {+componentStack?: ?string},
306 + ) => void,
307 + onCaughtError: (
308 + error: mixed,
309 + errorInfo: {
310 + +componentStack?: ?string,
311 + +errorBoundary?: ?React$Component<any, any>,
312 + },
313 + ) => void,
314 + onRecoverableError: (
315 + error: mixed,
316 + errorInfo: {+digest?: ?string, +componentStack?: ?string},
317 + ) => void,
318 transitionCallbacks: null | TransitionTracingCallbacks,
319 formState: ReactFormState<any, any> | null,
320 ): OpaqueRoot {
@@ -293,6 +328,8 @@ export function createHydrationContainer(
328 isStrictMode,
329 concurrentUpdatesByDefaultOverride,
330 identifierPrefix,
331 + onUncaughtError,
332 + onCaughtError,
333 onRecoverableError,
334 transitionCallbacks,
335 formState,
packages/react-reconciler/src/ReactFiberRoot.js
+21 -1
@@ -51,6 +51,8 @@ function FiberRootNode(
51 tag,
52 hydrate: any,
53 identifierPrefix: any,
54 + onUncaughtError: any,
55 + onCaughtError: any,
56 onRecoverableError: any,
57 formState: ReactFormState<any, any> | null,
58 ) {
@@ -83,6 +85,8 @@ function FiberRootNode(
85 this.hiddenUpdates = createLaneMap(null);
86
87 this.identifierPrefix = identifierPrefix;
88 + this.onUncaughtError = onUncaughtError;
89 + this.onCaughtError = onCaughtError;
90 this.onRecoverableError = onRecoverableError;
91
92 if (enableCache) {
@@ -143,7 +147,21 @@ export function createFiberRoot(
147 // them through the root constructor. Perhaps we should put them all into a
148 // single type, like a DynamicHostConfig that is defined by the renderer.
149 identifierPrefix: string,
146 - onRecoverableError: null | ((error: mixed) => void),
150 + onUncaughtError: (
151 + error: mixed,
152 + errorInfo: {+componentStack?: ?string},
153 + ) => void,
154 + onCaughtError: (
155 + error: mixed,
156 + errorInfo: {
157 + +componentStack?: ?string,
158 + +errorBoundary?: ?React$Component<any, any>,
159 + },
160 + ) => void,
161 + onRecoverableError: (
162 + error: mixed,
163 + errorInfo: {+digest?: ?string, +componentStack?: ?string},
164 + ) => void,
165 transitionCallbacks: null | TransitionTracingCallbacks,
166 formState: ReactFormState<any, any> | null,
167 ): FiberRoot {
@@ -153,6 +171,8 @@ export function createFiberRoot(
171 tag,
172 hydrate,
173 identifierPrefix,
174 + onUncaughtError,
175 + onCaughtError,
176 onRecoverableError,
177 formState,
178 ): any);
packages/react-reconciler/src/ReactFiberThrow.js
+28 -18
@@ -66,7 +66,7 @@ import {
66 renderDidSuspend,
67 } from './ReactFiberWorkLoop';
68 import {propagateParentContextChangesToDeferredTree} from './ReactFiberNewContext';
69 -import {logCapturedError} from './ReactFiberErrorLogger';
69 +import {logUncaughtError, logCaughtError} from './ReactFiberErrorLogger';
70 import {logComponentSuspended} from './DebugTracing';
71 import {isDevToolsPresent} from './ReactFiberDevToolsHook';
72 import {
@@ -85,7 +85,7 @@ import {noopSuspenseyCommitThenable} from './ReactFiberThenable';
85 import {REACT_POSTPONE_TYPE} from 'shared/ReactSymbols';
86
87 function createRootErrorUpdate(
88 - fiber: Fiber,
88 + root: FiberRoot,
89 errorInfo: CapturedValue<mixed>,
90 lane: Lane,
91 ): Update<mixed> {
@@ -96,18 +96,23 @@ function createRootErrorUpdate(
96 // being called "element".
97 update.payload = {element: null};
98 update.callback = () => {
99 - logCapturedError(fiber, errorInfo);
99 + logUncaughtError(root, errorInfo);
100 };
101 return update;
102 }
103
104 -function createClassErrorUpdate(
105 - fiber: Fiber,
106 - errorInfo: CapturedValue<mixed>,
107 - lane: Lane,
108 -): Update<mixed> {
104 +function createClassErrorUpdate(lane: Lane): Update<mixed> {
105 const update = createUpdate(lane);
106 update.tag = CaptureUpdate;
107 + return update;
108 +}
109 +
110 +function initializeClassErrorUpdate(
111 + update: Update<mixed>,
112 + root: FiberRoot,
113 + fiber: Fiber,
114 + errorInfo: CapturedValue<mixed>,
115 +): void {
116 const getDerivedStateFromError = fiber.type.getDerivedStateFromError;
117 if (typeof getDerivedStateFromError === 'function') {
118 const error = errorInfo.value;
@@ -118,7 +123,7 @@ function createClassErrorUpdate(
123 if (__DEV__) {
124 markFailedErrorBoundaryForHotReloading(fiber);
125 }
121 - logCapturedError(fiber, errorInfo);
126 + logCaughtError(root, fiber, errorInfo);
127 };
128 }
129
@@ -129,7 +134,7 @@ function createClassErrorUpdate(
134 if (__DEV__) {
135 markFailedErrorBoundaryForHotReloading(fiber);
136 }
132 - logCapturedError(fiber, errorInfo);
137 + logCaughtError(root, fiber, errorInfo);
138 if (typeof getDerivedStateFromError !== 'function') {
139 // To preserve the preexisting retry behavior of error boundaries,
140 // we keep track of which ones already failed during this batch.
@@ -159,7 +164,6 @@ function createClassErrorUpdate(
164 }
165 };
166 }
162 - return update;
167 }
168
169 function resetSuspendedComponent(sourceFiber: Fiber, rootRenderLanes: Lanes) {
@@ -561,7 +565,11 @@ function throwException(
565 workInProgress.flags |= ShouldCapture;
566 const lane = pickArbitraryLane(rootRenderLanes);
567 workInProgress.lanes = mergeLanes(workInProgress.lanes, lane);
564 - const update = createRootErrorUpdate(workInProgress, errorInfo, lane);
568 + const update = createRootErrorUpdate(
569 + workInProgress.stateNode,
570 + errorInfo,
571 + lane,
572 + );
573 enqueueCapturedUpdate(workInProgress, update);
574 return false;
575 }
@@ -581,11 +589,8 @@ function throwException(
589 const lane = pickArbitraryLane(rootRenderLanes);
590 workInProgress.lanes = mergeLanes(workInProgress.lanes, lane);
591 // Schedule the error boundary to re-render using updated state
584 - const update = createClassErrorUpdate(
585 - workInProgress,
586 - errorInfo,
587 - lane,
588 - );
592 + const update = createClassErrorUpdate(lane);
593 + initializeClassErrorUpdate(update, root, workInProgress, errorInfo);
594 enqueueCapturedUpdate(workInProgress, update);
595 return false;
596 }
@@ -600,4 +605,9 @@ function throwException(
605 return false;
606 }
607
603 -export {throwException, createRootErrorUpdate, createClassErrorUpdate};
608 +export {
609 + throwException,
610 + createRootErrorUpdate,
611 + createClassErrorUpdate,
612 + initializeClassErrorUpdate,
613 +};
packages/react-reconciler/src/ReactFiberWorkLoop.js
+12 -13
@@ -177,6 +177,7 @@ import {
177 throwException,
178 createRootErrorUpdate,
179 createClassErrorUpdate,
180 + initializeClassErrorUpdate,
181 } from './ReactFiberThrow';
182 import {
183 commitBeforeMutationEffects,
@@ -277,7 +278,7 @@ import {
278 } from './ReactFiberRootScheduler';
279 import {getMaskedContext, getUnmaskedContext} from './ReactFiberContext';
280 import {peekEntangledActionLane} from './ReactFiberAsyncAction';
280 -import {logCapturedError} from './ReactFiberErrorLogger';
281 +import {logUncaughtError} from './ReactFiberErrorLogger';
282
283 const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
284
@@ -1731,8 +1732,8 @@ function handleThrow(root: FiberRoot, thrownValue: any): void {
1732 if (erroredWork === null) {
1733 // This is a fatal error
1734 workInProgressRootExitStatus = RootFatalErrored;
1734 - logCapturedError(
1735 - root.current,
1735 + logUncaughtError(
1736 + root,
1737 createCapturedValueAtFiber(thrownValue, root.current),
1738 );
1739 return;
@@ -2552,10 +2553,7 @@ function panicOnRootError(root: FiberRoot, error: mixed) {
2553 // caught by an error boundary. This is a fatal error, or panic condition,
2554 // because we've run out of ways to recover.
2555 workInProgressRootExitStatus = RootFatalErrored;
2555 - logCapturedError(
2556 - root.current,
2557 - createCapturedValueAtFiber(error, root.current),
2558 - );
2556 + logUncaughtError(root, createCapturedValueAtFiber(error, root.current));
2557 // Set `workInProgress` to null. This represents advancing to the next
2558 // sibling, or the parent if there are no siblings. But since the root
2559 // has no siblings nor a parent, we set it to null. Usually this is
@@ -3356,7 +3354,11 @@ function captureCommitPhaseErrorOnRoot(
3354 error: mixed,
3355 ) {
3356 const errorInfo = createCapturedValueAtFiber(error, sourceFiber);
3359 - const update = createRootErrorUpdate(rootFiber, errorInfo, (SyncLane: Lane));
3357 + const update = createRootErrorUpdate(
3358 + rootFiber.stateNode,
3359 + errorInfo,
3360 + (SyncLane: Lane),
3361 + );
3362 const root = enqueueUpdate(rootFiber, update, (SyncLane: Lane));
3363 if (root !== null) {
3364 markRootUpdated(root, SyncLane);
@@ -3393,13 +3395,10 @@ export function captureCommitPhaseError(
3395 !isAlreadyFailedLegacyErrorBoundary(instance))
3396 ) {
3397 const errorInfo = createCapturedValueAtFiber(error, sourceFiber);
3396 - const update = createClassErrorUpdate(
3397 - fiber,
3398 - errorInfo,
3399 - (SyncLane: Lane),
3400 - );
3398 + const update = createClassErrorUpdate((SyncLane: Lane));
3399 const root = enqueueUpdate(fiber, update, (SyncLane: Lane));
3400 if (root !== null) {
3401 + initializeClassErrorUpdate(update, root, fiber, errorInfo);
3402 markRootUpdated(root, SyncLane);
3403 ensureRootIsScheduled(root);
3404 }
packages/react-reconciler/src/ReactInternalTypes.js
+12 -1
@@ -260,9 +260,20 @@ type BaseFiberRootProperties = {
260 // a reference to.
261 identifierPrefix: string,
262
263 + onUncaughtError: (
264 + error: mixed,
265 + errorInfo: {+componentStack?: ?string},
266 + ) => void,
267 + onCaughtError: (
268 + error: mixed,
269 + errorInfo: {
270 + +componentStack?: ?string,
271 + +errorBoundary?: ?React$Component<any, any>,
272 + },
273 + ) => void,
274 onRecoverableError: (
275 error: mixed,
265 - errorInfo: {digest?: ?string, componentStack?: ?string},
276 + errorInfo: {+digest?: ?string, +componentStack?: ?string},
277 ) => void,
278
279 formState: ReactFormState<any, any> | null,
packages/react-reconciler/src/__tests__/ReactConfigurableErrorLogging-test.js new
+229
@@ -0,0 +1,229 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @emails react-core
8 + */
9 +
10 +'use strict';
11 +
12 +let React;
13 +let ReactDOMClient;
14 +let Scheduler;
15 +let container;
16 +let act;
17 +
18 +async function fakeAct(cb) {
19 + // We don't use act/waitForThrow here because we want to observe how errors are reported for real.
20 + await cb();
21 + Scheduler.unstable_flushAll();
22 +}
23 +
24 +describe('ReactConfigurableErrorLogging', () => {
25 + beforeEach(() => {
26 + jest.resetModules();
27 + React = require('react');
28 + ReactDOMClient = require('react-dom/client');
29 + Scheduler = require('scheduler');
30 + container = document.createElement('div');
31 + if (__DEV__) {
32 + act = React.act;
33 + }
34 + });
35 +
36 + it('should log errors that occur during the begin phase', async () => {
37 + class ErrorThrowingComponent extends React.Component {
38 + constructor(props) {
39 + super(props);
40 + throw new Error('constructor error');
41 + }
42 + render() {
43 + return <div />;
44 + }
45 + }
46 + const uncaughtErrors = [];
47 + const caughtErrors = [];
48 + const root = ReactDOMClient.createRoot(container, {
49 + onUncaughtError(error, errorInfo) {
50 + uncaughtErrors.push(error, errorInfo);
51 + },
52 + onCaughtError(error, errorInfo) {
53 + caughtErrors.push(error, errorInfo);
54 + },
55 + });
56 + await fakeAct(() => {
57 + root.render(
58 + <div>
59 + <span>
60 + <ErrorThrowingComponent />
61 + </span>
62 + </div>,
63 + );
64 + });
65 +
66 + expect(uncaughtErrors).toEqual([
67 + expect.objectContaining({
68 + message: 'constructor error',
69 + }),
70 + expect.objectContaining({
71 + componentStack: expect.stringMatching(
72 + new RegExp(
73 + '\\s+(in|at) ErrorThrowingComponent (.*)\n' +
74 + '\\s+(in|at) span(.*)\n' +
75 + '\\s+(in|at) div(.*)',
76 + ),
77 + ),
78 + }),
79 + ]);
80 + expect(caughtErrors).toEqual([]);
81 + });
82 +
83 + it('should log errors that occur during the commit phase', async () => {
84 + class ErrorThrowingComponent extends React.Component {
85 + componentDidMount() {
86 + throw new Error('componentDidMount error');
87 + }
88 + render() {
89 + return <div />;
90 + }
91 + }
92 + const uncaughtErrors = [];
93 + const caughtErrors = [];
94 + const root = ReactDOMClient.createRoot(container, {
95 + onUncaughtError(error, errorInfo) {
96 + uncaughtErrors.push(error, errorInfo);
97 + },
98 + onCaughtError(error, errorInfo) {
99 + caughtErrors.push(error, errorInfo);
100 + },
101 + });
102 + await fakeAct(() => {
103 + root.render(
104 + <div>
105 + <span>
106 + <ErrorThrowingComponent />
107 + </span>
108 + </div>,
109 + );
110 + });
111 +
112 + expect(uncaughtErrors).toEqual([
113 + expect.objectContaining({
114 + message: 'componentDidMount error',
115 + }),
116 + expect.objectContaining({
117 + componentStack: expect.stringMatching(
118 + new RegExp(
119 + '\\s+(in|at) ErrorThrowingComponent (.*)\n' +
120 + '\\s+(in|at) span(.*)\n' +
121 + '\\s+(in|at) div(.*)',
122 + ),
123 + ),
124 + }),
125 + ]);
126 + expect(caughtErrors).toEqual([]);
127 + });
128 +
129 + it('should ignore errors thrown in log method to prevent cycle', async () => {
130 + class ErrorBoundary extends React.Component {
131 + state = {error: null};
132 + componentDidCatch(error) {
133 + this.setState({error});
134 + }
135 + render() {
136 + return this.state.error ? null : this.props.children;
137 + }
138 + }
139 + class ErrorThrowingComponent extends React.Component {
140 + render() {
141 + throw new Error('render error');
142 + }
143 + }
144 +
145 + const uncaughtErrors = [];
146 + const caughtErrors = [];
147 + const root = ReactDOMClient.createRoot(container, {
148 + onUncaughtError(error, errorInfo) {
149 + uncaughtErrors.push(error, errorInfo);
150 + },
151 + onCaughtError(error, errorInfo) {
152 + caughtErrors.push(error, errorInfo);
153 + throw new Error('onCaughtError error');
154 + },
155 + });
156 +
157 + const ref = React.createRef();
158 +
159 + await fakeAct(() => {
160 + root.render(
161 + <div>
162 + <ErrorBoundary ref={ref}>
163 + <span>
164 + <ErrorThrowingComponent />
165 + </span>
166 + </ErrorBoundary>
167 + </div>,
168 + );
169 + });
170 +
171 + expect(uncaughtErrors).toEqual([]);
172 + expect(caughtErrors).toEqual([
173 + expect.objectContaining({
174 + message: 'render error',
175 + }),
176 + expect.objectContaining({
177 + componentStack: expect.stringMatching(
178 + new RegExp(
179 + '\\s+(in|at) ErrorThrowingComponent (.*)\n' +
180 + '\\s+(in|at) span(.*)\n' +
181 + '\\s+(in|at) ErrorBoundary(.*)\n' +
182 + '\\s+(in|at) div(.*)',
183 + ),
184 + ),
185 + errorBoundary: ref.current,
186 + }),
187 + ]);
188 +
189 + // The error thrown in caughtError should be rethrown with a clean stack
190 + expect(() => {
191 + jest.runAllTimers();
192 + }).toThrow('onCaughtError error');
193 + });
194 +
195 + it('does not log errors when inside real act', async () => {
196 + function ErrorThrowingComponent() {
197 + throw new Error('render error');
198 + }
199 + const uncaughtErrors = [];
200 + const caughtErrors = [];
201 + const root = ReactDOMClient.createRoot(container, {
202 + onUncaughtError(error, errorInfo) {
203 + uncaughtErrors.push(error, errorInfo);
204 + },
205 + onCaughtError(error, errorInfo) {
206 + caughtErrors.push(error, errorInfo);
207 + },
208 + });
209 +
210 + if (__DEV__) {
211 + global.IS_REACT_ACT_ENVIRONMENT = true;
212 +
213 + await expect(async () => {
214 + await act(() => {
215 + root.render(
216 + <div>
217 + <span>
218 + <ErrorThrowingComponent />
219 + </span>
220 + </div>,
221 + );
222 + });
223 + }).rejects.toThrow('render error');
224 + }
225 +
226 + expect(uncaughtErrors).toEqual([]);
227 + expect(caughtErrors).toEqual([]);
228 + });
229 +});
packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js
+4
@@ -93,7 +93,11 @@ describe('ReactFiberHostContext', () => {
93 ConcurrentRoot,
94 null,
95 false,
96 + null,
97 '',
98 + () => {},
99 + () => {},
100 + () => {},
101 null,
102 );
103 act(() => {
packages/react-reconciler/src/forks/ReactFiberErrorDialog.native.js deleted
-37
@@ -1,37 +0,0 @@
1 -/**
2 - * Copyright (c) Meta Platforms, Inc. and affiliates.
3 - *
4 - * This source code is licensed under the MIT license found in the
5 - * LICENSE file in the root directory of this source tree.
6 - *
7 - * @flow
8 - */
9 -
10 -import type {Fiber} from '../ReactFiber';
11 -import type {CapturedValue} from '../ReactCapturedValue';
12 -
13 -import {ClassComponent} from '../ReactWorkTags';
14 -
15 -// Module provided by RN:
16 -import {ReactFiberErrorDialog as RNImpl} from 'react-native/Libraries/ReactPrivate/ReactNativePrivateInterface';
17 -
18 -if (typeof RNImpl.showErrorDialog !== 'function') {
19 - throw new Error(
20 - 'Expected ReactFiberErrorDialog.showErrorDialog to be a function.',
21 - );
22 -}
23 -
24 -export function showErrorDialog(
25 - boundary: Fiber,
26 - errorInfo: CapturedValue<mixed>,
27 -): boolean {
28 - const capturedError = {
29 - componentStack: errorInfo.stack !== null ? errorInfo.stack : '',
30 - error: errorInfo.value,
31 - errorBoundary:
32 - boundary !== null && boundary.tag === ClassComponent
33 - ? boundary.stateNode
34 - : null,
35 - };
36 - return RNImpl.showErrorDialog(capturedError);
37 -}
packages/react-reconciler/src/forks/ReactFiberErrorDialog.www.js deleted
-37
@@ -1,37 +0,0 @@
1 -/**
2 - * Copyright (c) Meta Platforms, Inc. and affiliates.
3 - *
4 - * This source code is licensed under the MIT license found in the
5 - * LICENSE file in the root directory of this source tree.
6 - *
7 - * @flow
8 - */
9 -
10 -import type {Fiber} from '../ReactFiber';
11 -import type {CapturedValue} from '../ReactCapturedValue';
12 -
13 -import {ClassComponent} from '../ReactWorkTags';
14 -
15 -// Provided by www
16 -const ReactFiberErrorDialogWWW = require('ReactFiberErrorDialog');
17 -
18 -if (typeof ReactFiberErrorDialogWWW.showErrorDialog !== 'function') {
19 - throw new Error(
20 - 'Expected ReactFiberErrorDialog.showErrorDialog to be a function.',
21 - );
22 -}
23 -
24 -export function showErrorDialog(
25 - boundary: Fiber,
26 - errorInfo: CapturedValue<mixed>,
27 -): boolean {
28 - const capturedError = {
29 - componentStack: errorInfo.stack !== null ? errorInfo.stack : '',
30 - error: errorInfo.value,
31 - errorBoundary:
32 - boundary !== null && boundary.tag === ClassComponent
33 - ? boundary.stateNode
34 - : null,
35 - };
36 - return ReactFiberErrorDialogWWW.showErrorDialog(capturedError);
37 -}
packages/react-test-renderer/src/ReactTestRenderer.js
+6 -8
@@ -23,6 +23,9 @@ import {
23 flushSync,
24 injectIntoDevTools,
25 batchedUpdates,
26 + defaultOnUncaughtError,
27 + defaultOnCaughtError,
28 + defaultOnRecoverableError,
29 } from 'react-reconciler/src/ReactFiberReconciler';
30 import {findCurrentFiberUsingSlowPath} from 'react-reconciler/src/ReactFiberTreeReflection';
31 import {
@@ -454,13 +457,6 @@ function propsMatch(props: Object, filter: Object): boolean {
457 return true;
458 }
459
457 -// $FlowFixMe[missing-local-annot]
458 -function onRecoverableError(error) {
459 - // TODO: Expose onRecoverableError option to userspace
460 - // eslint-disable-next-line react-internal/no-production-logging, react-internal/warning-args
461 - console.error(error);
462 -}
463 -
460 function create(
461 element: React$Element<any>,
462 options: TestRendererOptions,
@@ -522,7 +518,9 @@ function create(
518 isStrictMode,
519 concurrentUpdatesByDefault,
520 '',
525 - onRecoverableError,
521 + defaultOnUncaughtError,
522 + defaultOnCaughtError,
523 + defaultOnRecoverableError,
524 null,
525 );
526
packages/react-test-renderer/src/__tests__/ReactTestRenderer-test.internal.js
+2
@@ -98,6 +98,8 @@ describe('ReactTestRenderer', () => {
98 null,
99 expect.anything(),
100 expect.anything(),
101 + expect.anything(),
102 + expect.anything(),
103 null,
104 );
105 }
scripts/rollup/forks.js
-30
@@ -232,36 +232,6 @@ const forks = Object.freeze({
232 }
233 },
234
235 - // Different dialogs for caught errors.
236 - './packages/react-reconciler/src/ReactFiberErrorDialog.js': (
237 - bundleType,
238 - entry
239 - ) => {
240 - switch (bundleType) {
241 - case FB_WWW_DEV:
242 - case FB_WWW_PROD:
243 - case FB_WWW_PROFILING:
244 - // Use the www fork which shows an error dialog.
245 - return './packages/react-reconciler/src/forks/ReactFiberErrorDialog.www.js';
246 - case RN_OSS_DEV:
247 - case RN_OSS_PROD:
248 - case RN_OSS_PROFILING:
249 - case RN_FB_DEV:
250 - case RN_FB_PROD:
251 - case RN_FB_PROFILING:
252 - switch (entry) {
253 - case 'react-native-renderer':
254 - case 'react-native-renderer/fabric':
255 - // Use the RN fork which plays well with redbox.
256 - return './packages/react-reconciler/src/forks/ReactFiberErrorDialog.native.js';
257 - default:
258 - return null;
259 - }
260 - default:
261 - return null;
262 - }
263 - },
264 -
235 './packages/react-reconciler/src/ReactFiberConfig.js': (
236 bundleType,
237 entry,