Skip to content

Commit

Permalink
Be more defensive when recursing through error cause chain
Browse files Browse the repository at this point in the history
  • Loading branch information
AbhiPrasad committed Mar 22, 2023
1 parent e37e6e7 commit d0c99b2
Show file tree
Hide file tree
Showing 2 changed files with 54 additions and 3 deletions.
18 changes: 15 additions & 3 deletions packages/react/src/errorboundary.tsx
Expand Up @@ -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<Error, boolean>();

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

/**
Expand Down
39 changes: 39 additions & 0 deletions packages/react/test/errorboundary.test.tsx
Expand Up @@ -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(
<TestApp fallback={<p>You have hit an error</p>} onError={mockOnError} errorComp={<CustomBam />}>
<h1>children</h1>
</TestApp>,
);

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

Expand Down

0 comments on commit d0c99b2

Please sign in to comment.