Skip to content

Commit fd377d6

Browse files
authored
fix(react): Guard against non-error obj in ErrorBoundary (#6181)
1 parent c9128b0 commit fd377d6

File tree

2 files changed

+57
-2
lines changed

2 files changed

+57
-2
lines changed

packages/react/src/errorboundary.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { captureException, ReportDialogOptions, Scope, showReportDialog, withScope } from '@sentry/browser';
2-
import { logger } from '@sentry/utils';
2+
import { isError, logger } from '@sentry/utils';
33
import hoistNonReactStatics from 'hoist-non-react-statics';
44
import * as React from 'react';
55

@@ -75,7 +75,12 @@ class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundarySta
7575
// If on React version >= 17, create stack trace from componentStack param and links
7676
// to to the original error using `error.cause` otherwise relies on error param for stacktrace.
7777
// Linking errors requires the `LinkedErrors` integration be enabled.
78-
if (isAtLeastReact17(React.version)) {
78+
// See: https://reactjs.org/blog/2020/08/10/react-v17-rc.html#native-component-stacks
79+
//
80+
// Although `componentDidCatch` is typed to accept an `Error` object, it can also be invoked
81+
// with non-error objects. This is why we need to check if the error is an error-like object.
82+
// See: https://github.com/getsentry/sentry-javascript/issues/6167
83+
if (isAtLeastReact17(React.version) && isError(error)) {
7984
const errorBoundaryError = new Error(error.message);
8085
errorBoundaryError.name = `React ErrorBoundary ${errorBoundaryError.name}`;
8186
errorBoundaryError.stack = componentStack;

packages/react/test/errorboundary.test.tsx

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,56 @@ describe('ErrorBoundary', () => {
248248
expect(cause.message).toEqual(error.message);
249249
});
250250

251+
// Regression test against:
252+
// https://github.com/getsentry/sentry-javascript/issues/6167
253+
it('does not set cause if non Error objected is thrown', () => {
254+
const TestAppThrowingString: React.FC<ErrorBoundaryProps> = ({ children, ...props }) => {
255+
const [isError, setError] = React.useState(false);
256+
function StringBam(): JSX.Element {
257+
throw 'bam';
258+
}
259+
return (
260+
<ErrorBoundary
261+
{...props}
262+
onReset={(...args) => {
263+
setError(false);
264+
if (props.onReset) {
265+
props.onReset(...args);
266+
}
267+
}}
268+
>
269+
{isError ? <StringBam /> : children}
270+
<button
271+
data-testid="errorBtn"
272+
onClick={() => {
273+
setError(true);
274+
}}
275+
/>
276+
</ErrorBoundary>
277+
);
278+
};
279+
280+
render(
281+
<TestAppThrowingString fallback={<p>You have hit an error</p>}>
282+
<h1>children</h1>
283+
</TestAppThrowingString>,
284+
);
285+
286+
expect(mockCaptureException).toHaveBeenCalledTimes(0);
287+
288+
const btn = screen.getByTestId('errorBtn');
289+
fireEvent.click(btn);
290+
291+
expect(mockCaptureException).toHaveBeenCalledTimes(1);
292+
expect(mockCaptureException).toHaveBeenLastCalledWith('bam', {
293+
contexts: { react: { componentStack: expect.any(String) } },
294+
});
295+
296+
// Check if error.cause -> react component stack
297+
const error = mockCaptureException.mock.calls[0][0];
298+
expect(error.cause).not.toBeDefined();
299+
});
300+
251301
it('calls `beforeCapture()` when an error occurs', () => {
252302
const mockBeforeCapture = jest.fn();
253303

0 commit comments

Comments
 (0)