Skip to content

Commit

Permalink
Capture the source and not just the stack on first seen error (#31367)
Browse files Browse the repository at this point in the history
Otherwise we can't capture the owner stack at the right location when
there's a rethrow.
  • Loading branch information
sebmarkbage authored Oct 28, 2024
1 parent 02c0e82 commit 0bc3074
Show file tree
Hide file tree
Showing 3 changed files with 102 additions and 21 deletions.
10 changes: 8 additions & 2 deletions packages/react-noop-renderer/src/createReactNoop.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ type TextInstance = {
type HostContext = Object;
type CreateRootOptions = {
unstable_transitionCallbacks?: TransitionTracingCallbacks,
onUncaughtError?: (error: mixed, errorInfo: {componentStack: string}) => void,
onCaughtError?: (error: mixed, errorInfo: {componentStack: string}) => void,
...
};

Expand Down Expand Up @@ -1069,8 +1071,12 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
null,
false,
'',
NoopRenderer.defaultOnUncaughtError,
NoopRenderer.defaultOnCaughtError,
options && options.onUncaughtError
? options.onUncaughtError
: NoopRenderer.defaultOnUncaughtError,
options && options.onCaughtError
? options.onCaughtError
: NoopRenderer.defaultOnCaughtError,
onRecoverableError,
options && options.unstable_transitionCallbacks
? options.unstable_transitionCallbacks
Expand Down
40 changes: 21 additions & 19 deletions packages/react-reconciler/src/ReactCapturedValue.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import type {Fiber} from './ReactInternalTypes';

import {getStackByFiberInDevAndProd} from './ReactFiberComponentStack';

const CapturedStacks: WeakMap<any, string> = new WeakMap();
const CapturedStacks: WeakMap<any, CapturedValue<any>> = new WeakMap();

export type CapturedValue<+T> = {
+value: T,
Expand All @@ -25,36 +25,38 @@ export function createCapturedValueAtFiber<T>(
): CapturedValue<T> {
// If the value is an error, call this function immediately after it is thrown
// so the stack is accurate.
let stack;
if (typeof value === 'object' && value !== null) {
const capturedStack = CapturedStacks.get(value);
if (typeof capturedStack === 'string') {
stack = capturedStack;
} else {
stack = getStackByFiberInDevAndProd(source);
CapturedStacks.set(value, stack);
const existing = CapturedStacks.get(value);
if (existing !== undefined) {
return existing;
}
const captured = {
value,
source,
stack: getStackByFiberInDevAndProd(source),
};
CapturedStacks.set(value, captured);
return captured;
} else {
stack = getStackByFiberInDevAndProd(source);
return {
value,
source,
stack: getStackByFiberInDevAndProd(source),
};
}

return {
value,
source,
stack,
};
}

export function createCapturedValueFromError(
value: Error,
stack: null | string,
): CapturedValue<Error> {
if (typeof stack === 'string') {
CapturedStacks.set(value, stack);
}
return {
const captured = {
value,
source: null,
stack: stack,
};
if (typeof stack === 'string') {
CapturedStacks.set(value, captured);
}
return captured;
}
73 changes: 73 additions & 0 deletions packages/react-reconciler/src/__tests__/ReactErrorStacks-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,4 +100,77 @@ describe('ReactFragment', () => {
]),
]);
});

it('retains owner stacks when rethrowing an error', async () => {
function Foo() {
return (
<RethrowingBoundary>
<Bar />
</RethrowingBoundary>
);
}
function Bar() {
return <SomethingThatErrors />;
}
function SomethingThatErrors() {
throw new Error('uh oh');
}

class RethrowingBoundary extends React.Component {
static getDerivedStateFromError(error) {
throw error;
}

render() {
return this.props.children;
}
}

const errors = [];
class CatchingBoundary extends React.Component {
constructor() {
super();
this.state = {};
}
static getDerivedStateFromError(error) {
return {errored: true};
}
render() {
if (this.state.errored) {
return null;
}
return this.props.children;
}
}

ReactNoop.createRoot({
onCaughtError(error, errorInfo) {
errors.push(
error.message,
normalizeCodeLocInfo(errorInfo.componentStack),
React.captureOwnerStack
? normalizeCodeLocInfo(React.captureOwnerStack())
: null,
);
},
}).render(
<CatchingBoundary>
<Foo />
</CatchingBoundary>,
);
await waitForAll([]);
expect(errors).toEqual([
'uh oh',
componentStack([
'SomethingThatErrors',
'Bar',
'RethrowingBoundary',
'Foo',
'CatchingBoundary',
]),
gate(flags => flags.enableOwnerStacks) && __DEV__
? componentStack(['Bar', 'Foo'])
: null,
]);
});
});

0 comments on commit 0bc3074

Please sign in to comment.