Skip to content

Commit e37e6e7

Browse files
committed
feat(react): Handle case where error.cause already defined
1 parent 6426b58 commit e37e6e7

File tree

2 files changed

+60
-11
lines changed

2 files changed

+60
-11
lines changed

packages/react/src/errorboundary.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,13 @@ const INITIAL_STATE = {
6666
eventId: null,
6767
};
6868

69+
function setCause(error: Error & { cause?: Error }, cause: Error): void {
70+
if (error.cause) {
71+
return setCause(error.cause, cause);
72+
}
73+
error.cause = cause;
74+
}
75+
6976
/**
7077
* A ErrorBoundary component that logs errors to Sentry. Requires React >= 16.
7178
* NOTE: If you are a Sentry user, and you are seeing this stack frame, it means the
@@ -93,7 +100,7 @@ class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundarySta
93100
errorBoundaryError.stack = componentStack;
94101

95102
// Using the `LinkedErrors` integration to link the errors together.
96-
error.cause = errorBoundaryError;
103+
setCause(error, errorBoundaryError);
97104
}
98105

99106
if (beforeCapture) {

packages/react/test/errorboundary.test.tsx

Lines changed: 52 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,8 @@ import { fireEvent, render, screen } from '@testing-library/react';
33
import * as React from 'react';
44
import { useState } from 'react';
55

6-
import type {
7-
ErrorBoundaryProps} from '../src/errorboundary';
8-
import {
9-
ErrorBoundary,
10-
isAtLeastReact17,
11-
UNKNOWN_COMPONENT,
12-
withErrorBoundary,
13-
} from '../src/errorboundary';
6+
import type { ErrorBoundaryProps } from '../src/errorboundary';
7+
import { ErrorBoundary, isAtLeastReact17, UNKNOWN_COMPONENT, withErrorBoundary } from '../src/errorboundary';
148

159
const mockCaptureException = jest.fn();
1610
const mockShowReportDialog = jest.fn();
@@ -39,7 +33,13 @@ function Bam(): JSX.Element {
3933
return <Boo title={title} />;
4034
}
4135

42-
const TestApp: React.FC<ErrorBoundaryProps> = ({ children, ...props }) => {
36+
interface TestAppProps extends ErrorBoundaryProps {
37+
errorComp?: JSX.Element;
38+
}
39+
40+
const TestApp: React.FC<TestAppProps> = ({ children, errorComp, ...props }) => {
41+
// eslint-disable-next-line no-param-reassign
42+
const customErrorComp = errorComp || <Bam />;
4343
const [isError, setError] = React.useState(false);
4444
return (
4545
<ErrorBoundary
@@ -51,7 +51,7 @@ const TestApp: React.FC<ErrorBoundaryProps> = ({ children, ...props }) => {
5151
}
5252
}}
5353
>
54-
{isError ? <Bam /> : children}
54+
{isError ? customErrorComp : children}
5555
<button
5656
data-testid="errorBtn"
5757
onClick={() => {
@@ -299,6 +299,48 @@ describe('ErrorBoundary', () => {
299299
expect(error.cause).not.toBeDefined();
300300
});
301301

302+
it('handles when `error.cause` is nested', () => {
303+
const mockOnError = jest.fn();
304+
305+
function CustomBam(): JSX.Element {
306+
const firstError = new Error('bam');
307+
const secondError = new Error('bam2');
308+
const thirdError = new Error('bam3');
309+
// @ts-ignore Need to set cause on error
310+
secondError.cause = firstError;
311+
// @ts-ignore Need to set cause on error
312+
thirdError.cause = secondError;
313+
throw thirdError;
314+
}
315+
316+
render(
317+
<TestApp fallback={<p>You have hit an error</p>} onError={mockOnError} errorComp={<CustomBam />}>
318+
<h1>children</h1>
319+
</TestApp>,
320+
);
321+
322+
expect(mockOnError).toHaveBeenCalledTimes(0);
323+
expect(mockCaptureException).toHaveBeenCalledTimes(0);
324+
325+
const btn = screen.getByTestId('errorBtn');
326+
fireEvent.click(btn);
327+
328+
expect(mockCaptureException).toHaveBeenCalledTimes(1);
329+
expect(mockCaptureException).toHaveBeenLastCalledWith(expect.any(Error), {
330+
contexts: { react: { componentStack: expect.any(String) } },
331+
});
332+
333+
expect(mockOnError.mock.calls[0][0]).toEqual(mockCaptureException.mock.calls[0][0]);
334+
335+
const thirdError = mockCaptureException.mock.calls[0][0];
336+
const secondError = thirdError.cause;
337+
const firstError = secondError.cause;
338+
const cause = firstError.cause;
339+
expect(cause.stack).toEqual(mockCaptureException.mock.calls[0][1].contexts.react.componentStack);
340+
expect(cause.name).toContain('React ErrorBoundary');
341+
expect(cause.message).toEqual(thirdError.message);
342+
});
343+
302344
it('calls `beforeCapture()` when an error occurs', () => {
303345
const mockBeforeCapture = jest.fn();
304346

0 commit comments

Comments
 (0)