@samitouri / QOS-React-2 / commits / 0bc3074873

Capture the source and not just the stack on first seen error (#31367)

Otherwise we can't capture the owner stack at the right location when there's a rethrow.

Sebastian Markbåge committed Oct 28, 2024 at 13:59 UTC 0bc30748730063e561d87a24a4617526fdd38349
3 files changed +102 -21
packages/react-noop-renderer/src/createReactNoop.js
+8 -2
@@ -81,6 +81,8 @@ type TextInstance = {
81 type HostContext = Object;
82 type CreateRootOptions = {
83 unstable_transitionCallbacks?: TransitionTracingCallbacks,
84 + onUncaughtError?: (error: mixed, errorInfo: {componentStack: string}) => void,
85 + onCaughtError?: (error: mixed, errorInfo: {componentStack: string}) => void,
86 ...
87 };
88
@@ -1069,8 +1071,12 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
1071 null,
1072 false,
1073 '',
1072 - NoopRenderer.defaultOnUncaughtError,
1073 - NoopRenderer.defaultOnCaughtError,
1074 + options && options.onUncaughtError
1075 + ? options.onUncaughtError
1076 + : NoopRenderer.defaultOnUncaughtError,
1077 + options && options.onCaughtError
1078 + ? options.onCaughtError
1079 + : NoopRenderer.defaultOnCaughtError,
1080 onRecoverableError,
1081 options && options.unstable_transitionCallbacks
1082 ? options.unstable_transitionCallbacks
packages/react-reconciler/src/ReactCapturedValue.js
+21 -19
@@ -11,7 +11,7 @@ import type {Fiber} from './ReactInternalTypes';
11
12 import {getStackByFiberInDevAndProd} from './ReactFiberComponentStack';
13
14 -const CapturedStacks: WeakMap<any, string> = new WeakMap();
14 +const CapturedStacks: WeakMap<any, CapturedValue<any>> = new WeakMap();
15
16 export type CapturedValue<+T> = {
17 +value: T,
@@ -25,36 +25,38 @@ export function createCapturedValueAtFiber<T>(
25 ): CapturedValue<T> {
26 // If the value is an error, call this function immediately after it is thrown
27 // so the stack is accurate.
28 - let stack;
28 if (typeof value === 'object' && value !== null) {
30 - const capturedStack = CapturedStacks.get(value);
31 - if (typeof capturedStack === 'string') {
32 - stack = capturedStack;
33 - } else {
34 - stack = getStackByFiberInDevAndProd(source);
35 - CapturedStacks.set(value, stack);
29 + const existing = CapturedStacks.get(value);
30 + if (existing !== undefined) {
31 + return existing;
32 }
33 + const captured = {
34 + value,
35 + source,
36 + stack: getStackByFiberInDevAndProd(source),
37 + };
38 + CapturedStacks.set(value, captured);
39 + return captured;
40 } else {
38 - stack = getStackByFiberInDevAndProd(source);
41 + return {
42 + value,
43 + source,
44 + stack: getStackByFiberInDevAndProd(source),
45 + };
46 }
40 -
41 - return {
42 - value,
43 - source,
44 - stack,
45 - };
47 }
48
49 export function createCapturedValueFromError(
50 value: Error,
51 stack: null | string,
52 ): CapturedValue<Error> {
52 - if (typeof stack === 'string') {
53 - CapturedStacks.set(value, stack);
54 - }
55 - return {
53 + const captured = {
54 value,
55 source: null,
56 stack: stack,
57 };
58 + if (typeof stack === 'string') {
59 + CapturedStacks.set(value, captured);
60 + }
61 + return captured;
62 }
packages/react-reconciler/src/__tests__/ReactErrorStacks-test.js
+73
@@ -100,4 +100,77 @@ describe('ReactFragment', () => {
100 ]),
101 ]);
102 });
103 +
104 + it('retains owner stacks when rethrowing an error', async () => {
105 + function Foo() {
106 + return (
107 + <RethrowingBoundary>
108 + <Bar />
109 + </RethrowingBoundary>
110 + );
111 + }
112 + function Bar() {
113 + return <SomethingThatErrors />;
114 + }
115 + function SomethingThatErrors() {
116 + throw new Error('uh oh');
117 + }
118 +
119 + class RethrowingBoundary extends React.Component {
120 + static getDerivedStateFromError(error) {
121 + throw error;
122 + }
123 +
124 + render() {
125 + return this.props.children;
126 + }
127 + }
128 +
129 + const errors = [];
130 + class CatchingBoundary extends React.Component {
131 + constructor() {
132 + super();
133 + this.state = {};
134 + }
135 + static getDerivedStateFromError(error) {
136 + return {errored: true};
137 + }
138 + render() {
139 + if (this.state.errored) {
140 + return null;
141 + }
142 + return this.props.children;
143 + }
144 + }
145 +
146 + ReactNoop.createRoot({
147 + onCaughtError(error, errorInfo) {
148 + errors.push(
149 + error.message,
150 + normalizeCodeLocInfo(errorInfo.componentStack),
151 + React.captureOwnerStack
152 + ? normalizeCodeLocInfo(React.captureOwnerStack())
153 + : null,
154 + );
155 + },
156 + }).render(
157 + <CatchingBoundary>
158 + <Foo />
159 + </CatchingBoundary>,
160 + );
161 + await waitForAll([]);
162 + expect(errors).toEqual([
163 + 'uh oh',
164 + componentStack([
165 + 'SomethingThatErrors',
166 + 'Bar',
167 + 'RethrowingBoundary',
168 + 'Foo',
169 + 'CatchingBoundary',
170 + ]),
171 + gate(flags => flags.enableOwnerStacks) && __DEV__
172 + ? componentStack(['Bar', 'Foo'])
173 + : null,
174 + ]);
175 + });
176 });