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
});