Updated (originally published )
Testing React Component Error Boundaries
Update: I’ve revisited this post for React 19, React Testing Library 16 and Vitest 4. React 19 changed how render errors are reported, so a few of the snippets and notes below have been updated.
I’ve been working with error boundaries in React. The basic idea is that you can catch any thrown errors during the render phase of your application without disrupting the entire render. This can be helpful to avoid error messages that would otherwise disrupt the user’s flow:

You might structure your code to look like this:
<main>
<Header />
<Navigation />
<ErrorBoundary>
<Child />
</ErrorBoundary>
</main>
With this structure, if any of the child components throw an error during rendering, the error boundary will catch it.
It’s worth noting that error boundaries won’t catch errors inside event handlers.
I won’t go into how to create an error boundary component here since there’s a great example in the docs. It’s still the case (even in React 19) that a class component is the only built-in way to define one - if you’d rather not write a class yourself, the react-error-boundary package wraps it all up for you.
One challenge I faced was testing the component that throws the error in the first place.
Testing a thrown error on render
We can test an error that’s thrown during the initial render:
const Foo: FC = () => {
throw new Error('Oh no');
};
Like so:
test('component should throw', () => {
expect(() => render(<Foo />)).toThrow();
});
This works because React Testing Library wraps render in act, which re-throws the error so toThrow can catch it.
On React 19 the test passes cleanly - errors in render are no longer re-thrown and double-logged. On React 18 and earlier, the test will pass but there will be some noise in the output:
Error: Uncaught [Error: Oh no]
So we need to spyOn the console error:
test('component should throw', () => {
vi.spyOn(console, 'error').mockImplementation(() => null);
expect(() => render(<Foo />)).toThrow();
});
Our test should then pass.
Testing an asynchronously thrown error
If your component throws an error asynchronously, our approach will differ. Error boundaries won’t catch an error thrown directly inside an async callback, so the trick is to store it in state and re-throw it during render:
const Foo: FC = () => {
const [error, setError] = useState<Error | null>(null);
if (error) {
throw error;
}
useEffect(() => {
fetch('foo-endpoint').catch((err) => setError(new Error(err.message)));
}, []);
return null;
};
If we try to test it in the same way as the synchronous example, we’ll hit this error:
AssertionError: expected [Function] to throw an error
This is because the error happens after the initial render. In a browser, this will be caught by our error boundary so that’s how we can test it:
test('async component should throw', async () => {
vi.spyOn(console, 'error').mockImplementation(() => null);
render(
<ErrorBoundary
ErrorComponent={<div>Error Boundary</div>}
>
<Foo />
</ErrorBoundary>
);
await waitFor(() => {
expect(screen.getByText('Error Boundary')).toBeVisible();
});
});
In this example, we still need to spyOn the console because the test will pass, but React logs any error caught by an error boundary via console.error. We then wrap our component in an error boundary with a mocked component. We then use react-testing-librarys waitFor to wait for the async method to throw and update the component.
If you’re on React 19 with React Testing Library 16 or above, there’s a tidier alternative to spying on the console: render accepts an onCaughtError option which silences React’s default logging for errors caught by a boundary:
test('async component should throw', async () => {
render(
<ErrorBoundary
ErrorComponent={<div>Error Boundary</div>}
>
<Foo />
</ErrorBoundary>,
{ onCaughtError: () => null }
);
await waitFor(() => {
expect(screen.getByText('Error Boundary')).toBeVisible();
});
});