diff --git a/packages/react-router/src/CatchBoundary.tsx b/packages/react-router/src/CatchBoundary.tsx index f55a250ab1..3b10001b24 100644 --- a/packages/react-router/src/CatchBoundary.tsx +++ b/packages/react-router/src/CatchBoundary.tsx @@ -11,25 +11,26 @@ export class CatchBoundary extends React.Component<{ errorComponent?: ErrorRouteComponent onCatch?: (error: Error, errorInfo: ErrorInfo) => void }> { - state = { error: null } as { error: Error | null; resetKey?: unknown } + // Wrapping caught values keeps every possible thrown value truthy. + state = { error: 0 } as { error: [Error] | 0; resetKey?: unknown } static getDerivedStateFromProps( props: { getResetKey: () => unknown }, - state: { resetKey?: unknown; error: Error | null }, + state: { resetKey?: unknown; error: [Error] | 0 }, ) { const resetKey = props.getResetKey() if (state.error && state.resetKey !== resetKey) { - return { resetKey, error: null } + return { resetKey, error: 0 } } return { resetKey } } static getDerivedStateFromError(error: Error) { - return { error } + return { error: [error] } } reset = () => { - this.setState({ error: null }) + this.setState({ error: 0 }) } componentDidCatch(error: Error, errorInfo: ErrorInfo) { this.props.onCatch?.(error, errorInfo) @@ -40,7 +41,7 @@ export class CatchBoundary extends React.Component<{ const element = React.createElement( this.props.errorComponent ?? ErrorComponent, { - error, + error: error[0], reset: this.reset, }, ) @@ -88,7 +89,7 @@ export function ErrorComponent({ error }: { error: any }) { overflow: 'auto', }} > - {error.message ? {error.message} : null} + {error?.message ? {error.message} : null} ) : null} diff --git a/packages/react-router/src/Matches.tsx b/packages/react-router/src/Matches.tsx index cada2373ff..576c40a16f 100644 --- a/packages/react-router/src/Matches.tsx +++ b/packages/react-router/src/Matches.tsx @@ -115,7 +115,7 @@ function MatchesInner() { console.warn( `Warning: The following error wasn't caught by any route! At the very least, consider setting an 'errorComponent' in your RootRoute!`, ) - console.warn(`Warning: ${error.message || error.toString()}`) + console.warn('Warning:', error) } : undefined } diff --git a/packages/react-router/tests/errorComponent.test.tsx b/packages/react-router/tests/errorComponent.test.tsx index b5ee4b6709..93586106a3 100644 --- a/packages/react-router/tests/errorComponent.test.tsx +++ b/packages/react-router/tests/errorComponent.test.tsx @@ -2,6 +2,7 @@ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import { act, cleanup, fireEvent, render, screen } from '@testing-library/react' import { + CatchBoundary, HeadContent, Link, Outlet, @@ -422,6 +423,62 @@ test('errorComponent receives primitive errors thrown from beforeLoad', async () expect(screen.queryByText('About route content')).not.toBeInTheDocument() }) +test.each([ + ['false', false], + ['zero', 0], + ['negative zero', -0], + ['bigint zero', 0n], + ['empty string', ''], + ['null', null], + ['undefined', undefined], + ['NaN', NaN], +] as const)('CatchBoundary renders falsy thrown value %s', (_, thrown) => { + vi.spyOn(console, 'error').mockImplementation(() => {}) + const onCatch = vi.fn() + + function ThrowFalsy(): never { + throw thrown + } + + render( + 0} + errorComponent={({ error }) => ( +
{Object.is(error, thrown) ? 'Caught value' : 'Wrong value'}
+ )} + onCatch={onCatch} + > + +
, + ) + + expect(screen.getByText('Caught value')).toBeInTheDocument() + expect(screen.queryByText('Wrong value')).not.toBeInTheDocument() + expect(onCatch).toHaveBeenCalledWith(thrown, expect.anything()) +}) + +test.each([ + ['null', null], + ['undefined', undefined], +] as const)('default error UI renders thrown %s', async (_, thrown) => { + vi.spyOn(console, 'error').mockImplementation(() => {}) + vi.spyOn(console, 'warn').mockImplementation(() => {}) + + function ThrowFalsy(): never { + throw thrown + } + + const rootRoute = createRootRoute({ component: ThrowFalsy }) + const router = createRouter({ + routeTree: rootRoute, + history: createMemoryHistory({ initialEntries: ['/'] }), + }) + + render() + + expect(await screen.findByText('Something went wrong!')).toBeInTheDocument() +}) + test.each(['beforeLoad', 'loader'] as const)( 'a Promise synchronously thrown from %s renders the route error UI', async (hook) => {