diff --git a/packages/react/src/errorboundary.tsx b/packages/react/src/errorboundary.tsx
index d7312fdeffbb..377eedc7fd47 100644
--- a/packages/react/src/errorboundary.tsx
+++ b/packages/react/src/errorboundary.tsx
@@ -67,10 +67,22 @@ const INITIAL_STATE = {
};
function setCause(error: Error & { cause?: Error }, cause: Error): void {
- if (error.cause) {
- return setCause(error.cause, cause);
+ const seenErrors = new Map();
+
+ function recurse(error: Error & { cause?: Error }, cause: Error): void {
+ // If we've already seen the error, there is a recursive loop somewhere in the error's
+ // cause chain. Let's just bail out then to prevent a stack overflow.
+ if (seenErrors.has(error)) {
+ return;
+ }
+ if (error.cause) {
+ seenErrors.set(error, true);
+ return recurse(error.cause, cause);
+ }
+ error.cause = cause;
}
- error.cause = cause;
+
+ recurse(error, cause);
}
/**
diff --git a/packages/react/test/errorboundary.test.tsx b/packages/react/test/errorboundary.test.tsx
index 3f9284d03efa..251112242313 100644
--- a/packages/react/test/errorboundary.test.tsx
+++ b/packages/react/test/errorboundary.test.tsx
@@ -341,6 +341,45 @@ describe('ErrorBoundary', () => {
expect(cause.message).toEqual(thirdError.message);
});
+ it('handles when `error.cause` is recursive', () => {
+ const mockOnError = jest.fn();
+
+ function CustomBam(): JSX.Element {
+ const firstError = new Error('bam');
+ const secondError = new Error('bam2');
+ // @ts-ignore Need to set cause on error
+ firstError.cause = secondError;
+ // @ts-ignore Need to set cause on error
+ secondError.cause = firstError;
+ throw firstError;
+ }
+
+ render(
+ You have hit an error
} onError={mockOnError} errorComp={}>
+ children
+ ,
+ );
+
+ expect(mockOnError).toHaveBeenCalledTimes(0);
+ expect(mockCaptureException).toHaveBeenCalledTimes(0);
+
+ const btn = screen.getByTestId('errorBtn');
+ fireEvent.click(btn);
+
+ expect(mockCaptureException).toHaveBeenCalledTimes(1);
+ expect(mockCaptureException).toHaveBeenLastCalledWith(expect.any(Error), {
+ contexts: { react: { componentStack: expect.any(String) } },
+ });
+
+ expect(mockOnError.mock.calls[0][0]).toEqual(mockCaptureException.mock.calls[0][0]);
+
+ const error = mockCaptureException.mock.calls[0][0];
+ const cause = error.cause;
+ // We need to make sure that recursive error.cause does not cause infinite loop
+ expect(cause.stack).not.toEqual(mockCaptureException.mock.calls[0][1].contexts.react.componentStack);
+ expect(cause.name).not.toContain('React ErrorBoundary');
+ });
+
it('calls `beforeCapture()` when an error occurs', () => {
const mockBeforeCapture = jest.fn();