- {error.message}
+ {error instanceof Error ? error.message : String(error)}
{
// Reset the router error boundary
@@ -630,7 +630,7 @@ export const Route = createFileRoute('/posts')({
return (
- {error.message}
+ {error instanceof Error ? error.message : String(error)}
{
// Invalidate the route to reload the loader, which will also reset the error boundary
diff --git a/packages/react-router/src/CatchBoundary.tsx b/packages/react-router/src/CatchBoundary.tsx
index f55a250ab1d..1015a9e116e 100644
--- a/packages/react-router/src/CatchBoundary.tsx
+++ b/packages/react-router/src/CatchBoundary.tsx
@@ -9,29 +9,30 @@ export class CatchBoundary extends React.Component<{
getResetKey: () => unknown
children: React.ReactNode
errorComponent?: ErrorRouteComponent
- onCatch?: (error: Error, errorInfo: ErrorInfo) => void
+ onCatch?: (error: unknown, 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: [unknown] | 0; resetKey?: unknown }
static getDerivedStateFromProps(
props: { getResetKey: () => unknown },
- state: { resetKey?: unknown; error: Error | null },
+ state: { resetKey?: unknown; error: [unknown] | 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 }
+ static getDerivedStateFromError(error: unknown) {
+ return { error: [error] }
}
reset = () => {
- this.setState({ error: null })
+ this.setState({ error: 0 })
}
- componentDidCatch(error: Error, errorInfo: ErrorInfo) {
+ componentDidCatch(error: unknown, errorInfo: ErrorInfo) {
this.props.onCatch?.(error, errorInfo)
}
render() {
@@ -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,
},
)
@@ -54,7 +55,7 @@ export class CatchBoundary extends React.Component<{
}
}
-export function ErrorComponent({ error }: { error: any }) {
+export function ErrorComponent({ error }: { error: unknown }) {
const [show, setShow] = React.useState(process.env.NODE_ENV !== 'production')
return (
@@ -88,7 +89,9 @@ export function ErrorComponent({ error }: { error: any }) {
overflow: 'auto',
}}
>
- {error.message ? {error.message} : null}
+ {(error as { message?: string } | null)?.message ? (
+ {(error as { message: string }).message}
+ ) : null}
) : null}
diff --git a/packages/react-router/src/Match.tsx b/packages/react-router/src/Match.tsx
index a0bfa89d552..97b8f795892 100644
--- a/packages/react-router/src/Match.tsx
+++ b/packages/react-router/src/Match.tsx
@@ -236,7 +236,7 @@ export const MatchInner = React.memo(function MatchInnerImpl({
ErrorComponent
const errorElement = (
React.ReactElement
- onCatch?: (error: Error, errorInfo: ErrorInfo) => void
+ onCatch?: (error: NotFoundError, errorInfo: ErrorInfo) => void
children: React.ReactNode
}) {
const router = useRouter()
diff --git a/packages/react-router/src/router.ts b/packages/react-router/src/router.ts
index 178eace31bd..de5e89a9fbc 100644
--- a/packages/react-router/src/router.ts
+++ b/packages/react-router/src/router.ts
@@ -73,7 +73,7 @@ declare module '@tanstack/router-core' {
* @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#defaultoncatch-property)
* @link [Guide](https://tanstack.com/router/latest/docs/framework/react/guide/data-loading#handling-errors-with-routeoptionsoncatch)
*/
- defaultOnCatch?: (error: Error, errorInfo: React.ErrorInfo) => void
+ defaultOnCatch?: (error: unknown, errorInfo: React.ErrorInfo) => void
}
}
diff --git a/packages/react-router/tests/errorComponent.test.tsx b/packages/react-router/tests/errorComponent.test.tsx
index b5ee4b6709c..6ad5a4e4a26 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,
@@ -23,7 +24,11 @@ import {
import type { ErrorComponentProps, RouterHistory } from '../src'
function MyErrorComponent(props: ErrorComponentProps) {
- return Error: {props.error.message}
+ return Error: {getErrorMessage(props.error)}
+}
+
+function getErrorMessage(error: unknown) {
+ return error instanceof Error ? error.message : String(error)
}
async function asyncToThrowFn() {
@@ -324,7 +329,9 @@ test('ancestor route errorComponent resets when a background child generation re
let loaderCalls = 0
const rootRoute = createRootRoute({
component: Outlet,
- errorComponent: ({ error }) => Ancestor error: {error.message}
,
+ errorComponent: ({ error }) => (
+ Ancestor error: {getErrorMessage(error)}
+ ),
})
const childRoute = createRoute({
getParentRoute: () => rootRoute,
@@ -422,6 +429,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) => {
@@ -736,7 +799,9 @@ test('#4684: SSR renders head content when beforeLoad throws', async () => {
component: function FailingRoute() {
return Route content
},
- errorComponent: ({ error }) => Error UI: {error.message}
,
+ errorComponent: ({ error }) => (
+ Error UI: {getErrorMessage(error)}
+ ),
})
const handler = createRequestHandler({
diff --git a/packages/react-router/tests/issue-4476-react-query-cancellation.test.tsx b/packages/react-router/tests/issue-4476-react-query-cancellation.test.tsx
index 039608ffb2a..824fcc1fb78 100644
--- a/packages/react-router/tests/issue-4476-react-query-cancellation.test.tsx
+++ b/packages/react-router/tests/issue-4476-react-query-cancellation.test.tsx
@@ -77,7 +77,11 @@ test('#4476: pending navigation keeps the query observer mounted and its fetchQu
},
errorComponent: ({ error }) => {
routeError(error)
- return {error.name}
+ return (
+
+ {error instanceof Error ? error.name : String(error)}
+
+ )
},
component: () => {
const { data } = pageTwoRoute.useRouteContext()
diff --git a/packages/react-router/tests/issue-6107-lazy-chunk-error-component.test.tsx b/packages/react-router/tests/issue-6107-lazy-chunk-error-component.test.tsx
index 22440fe1190..138e7f5716f 100644
--- a/packages/react-router/tests/issue-6107-lazy-chunk-error-component.test.tsx
+++ b/packages/react-router/tests/issue-6107-lazy-chunk-error-component.test.tsx
@@ -59,7 +59,11 @@ test('#6107: lazy chunk hover failure is non-fatal and navigation renders defaul
defaultPreloadDelay: 0,
defaultErrorComponent: ({ error }) => {
defaultErrorRendered(error)
- return {error.message}
+ return (
+
+ {error instanceof Error ? error.message : String(error)}
+
+ )
},
})
const preloadRoute = vi.spyOn(router, 'preloadRoute')
diff --git a/packages/react-router/tests/issue-6371-search-default-normalization-abort.test.tsx b/packages/react-router/tests/issue-6371-search-default-normalization-abort.test.tsx
index 7698d117a6a..836ff045bc4 100644
--- a/packages/react-router/tests/issue-6371-search-default-normalization-abort.test.tsx
+++ b/packages/react-router/tests/issue-6371-search-default-normalization-abort.test.tsx
@@ -73,7 +73,11 @@ test('#6371: initial search defaults produce one live canonical loader', async (
),
errorComponent: ({ error }) => {
errorComponentRendered(error)
- return {error.message}
+ return (
+
+ {error instanceof Error ? error.message : String(error)}
+
+ )
},
})
const history = createMemoryHistory({ initialEntries: ['/about'] })
diff --git a/packages/react-router/tests/issue-7635-error-head-after-navigation.test.tsx b/packages/react-router/tests/issue-7635-error-head-after-navigation.test.tsx
index 56aed7b375d..23bc0aa5f37 100644
--- a/packages/react-router/tests/issue-7635-error-head-after-navigation.test.tsx
+++ b/packages/react-router/tests/issue-7635-error-head-after-navigation.test.tsx
@@ -63,7 +63,11 @@ test('#7635: a parent beforeLoad error replaces the previous child title', async
component: Outlet,
errorComponent: ({ error }) => {
appErrorRendered(error)
- return {error.message}
+ return (
+
+ {error instanceof Error ? error.message : String(error)}
+
+ )
},
})
const childRoute = createRoute({
diff --git a/packages/react-router/tests/issue-7638-invalidate-transition-error.test.tsx b/packages/react-router/tests/issue-7638-invalidate-transition-error.test.tsx
index 9f1c0eec3f5..e80f073896f 100644
--- a/packages/react-router/tests/issue-7638-invalidate-transition-error.test.tsx
+++ b/packages/react-router/tests/issue-7638-invalidate-transition-error.test.tsx
@@ -95,7 +95,14 @@ function setup({ failVia }: { failVia: 'render' | 'loader' }) {
history: createMemoryHistory({ initialEntries: ['/test'] }),
defaultErrorComponent: (props: ErrorComponentProps) => {
errorRenders++
- return error: {props.error.message}
+ return (
+
+ error:{' '}
+ {props.error instanceof Error
+ ? props.error.message
+ : String(props.error)}
+
+ )
},
})
diff --git a/packages/react-router/tests/lazy/error.tsx b/packages/react-router/tests/lazy/error.tsx
index 6ef7036e33f..271f6a058ec 100644
--- a/packages/react-router/tests/lazy/error.tsx
+++ b/packages/react-router/tests/lazy/error.tsx
@@ -3,6 +3,10 @@ import { createLazyRoute } from '../../src'
export function Route(id: string) {
return createLazyRoute(id)({
component: () => About route content
,
- errorComponent: ({ error }) => Lazy Error: {error.message}
,
+ errorComponent: ({ error }) => (
+
+ Lazy Error: {error instanceof Error ? error.message : String(error)}
+
+ ),
})
}
diff --git a/packages/react-router/tests/loaders.test.tsx b/packages/react-router/tests/loaders.test.tsx
index 6859b963e25..b96931d61ae 100644
--- a/packages/react-router/tests/loaders.test.tsx
+++ b/packages/react-router/tests/loaders.test.tsx
@@ -922,7 +922,9 @@ test('reproducer for #6388 - rapid navigation between parameterized routes shoul
errorComponentRenderCount(error)
return (
- Error Component: {error.message} | Name: {error.name}
+ Error Component:{' '}
+ {error instanceof Error ? error.message : String(error)} | Name:{' '}
+ {error instanceof Error ? error.name : typeof error}
)
},
diff --git a/packages/react-router/tests/redirect.test.tsx b/packages/react-router/tests/redirect.test.tsx
index 3cd6a3d88f4..ea8cd30dd44 100644
--- a/packages/react-router/tests/redirect.test.tsx
+++ b/packages/react-router/tests/redirect.test.tsx
@@ -28,6 +28,10 @@ import type { RouterHistory } from '../src'
let history: RouterHistory
+function getErrorMessage(error: unknown) {
+ return error instanceof Error ? error.message : String(error)
+}
+
beforeEach(() => {
history = createBrowserHistory()
expect(window.location.pathname).toBe('/')
@@ -95,7 +99,7 @@ describe('redirect', () => {
})
},
errorComponent: ({ error }) => (
- {error.message}
+ {getErrorMessage(error)}
),
})
const targetRoute = createRoute({
@@ -132,7 +136,7 @@ describe('redirect', () => {
})
const rootRoute = createRootRoute({
errorComponent: ({ error }) => (
- Root: {error.message}
+ Root: {getErrorMessage(error)}
),
})
const indexRoute = createRoute({
@@ -140,7 +144,7 @@ describe('redirect', () => {
path: '/',
loader,
errorComponent: ({ error }) => (
- Index: {error.message}
+ Index: {getErrorMessage(error)}
),
})
const router = createRouter({
@@ -168,7 +172,7 @@ describe('redirect', () => {
})
const rootRoute = createRootRoute({
errorComponent: ({ error }) => (
- Root: {error.message}
+ Root: {getErrorMessage(error)}
),
})
const indexRoute = createRoute({
@@ -176,7 +180,7 @@ describe('redirect', () => {
path: '/',
loader: indexLoader,
errorComponent: ({ error }) => (
- Index: {error.message}
+ Index: {getErrorMessage(error)}
),
})
const otherRoute = createRoute({
@@ -184,7 +188,7 @@ describe('redirect', () => {
path: '/other',
loader: otherLoader,
errorComponent: ({ error }) => (
- Other: {error.message}
+ Other: {getErrorMessage(error)}
),
})
const router = createRouter({
diff --git a/packages/react-router/tests/router.test.tsx b/packages/react-router/tests/router.test.tsx
index 43c4d474239..926e2c3b8f6 100644
--- a/packages/react-router/tests/router.test.tsx
+++ b/packages/react-router/tests/router.test.tsx
@@ -1857,7 +1857,7 @@ describe('search params in URL', () => {
describe.each(testCases)('search param validation', (validateSearch) => {
it('does not throw an error when the search param is valid', async () => {
- let errorSpy: Error | undefined
+ let errorSpy: unknown
const rootRoute = createRootRoute({
validateSearch,
errorComponent: ({ error }) => {
@@ -1877,7 +1877,7 @@ describe('search params in URL', () => {
})
it('throws an error when the search param is not valid', async () => {
- let errorSpy: Error | undefined
+ let errorSpy: unknown
const rootRoute = createRootRoute({
validateSearch,
errorComponent: ({ error }) => {
@@ -1892,7 +1892,9 @@ describe('search params in URL', () => {
await act(() => router.load())
expect(errorSpy).toBeInstanceOf(SearchParamError)
- expect(errorSpy?.cause).toBeInstanceOf(TestValidationError)
+ expect(
+ errorSpy instanceof Error ? errorSpy.cause : undefined,
+ ).toBeInstanceOf(TestValidationError)
})
})
})
diff --git a/packages/router-core/src/route.ts b/packages/router-core/src/route.ts
index f2543892677..b7e36880716 100644
--- a/packages/router-core/src/route.ts
+++ b/packages/router-core/src/route.ts
@@ -1298,7 +1298,7 @@ export interface UpdatableRouteOptions<
postSearchFilters?: Array<
SearchFilter>
>
- onCatch?: (error: Error) => void
+ onCatch?: (error: unknown) => void
onError?: (err: any) => void
// These functions are called as route matches are loaded, stick around and leave the active
// matches
@@ -1601,7 +1601,7 @@ export type ErrorRouteProps = {
reset: () => void
}
-export type ErrorComponentProps = {
+export type ErrorComponentProps = {
error: TError
info?: { componentStack: string }
reset: () => void
diff --git a/packages/router-core/tests/errorComponentProps.test-d.ts b/packages/router-core/tests/errorComponentProps.test-d.ts
new file mode 100644
index 00000000000..23df7cdcfa7
--- /dev/null
+++ b/packages/router-core/tests/errorComponentProps.test-d.ts
@@ -0,0 +1,7 @@
+import { expectTypeOf, test } from 'vitest'
+import type { ErrorComponentProps } from '../src'
+
+test('ErrorComponentProps defaults error to unknown', () => {
+ expectTypeOf().toEqualTypeOf()
+ expectTypeOf['error']>().toEqualTypeOf()
+})
diff --git a/packages/solid-router/src/CatchBoundary.tsx b/packages/solid-router/src/CatchBoundary.tsx
index a69c4859238..c0bb6519314 100644
--- a/packages/solid-router/src/CatchBoundary.tsx
+++ b/packages/solid-router/src/CatchBoundary.tsx
@@ -8,7 +8,7 @@ export function CatchBoundary(
getResetKey: () => unknown
children: Solid.JSX.Element
errorComponent?: ErrorRouteComponent
- onCatch?: (error: Error) => void
+ onCatch?: (error: unknown) => void
} & Solid.ParentProps,
) {
return (
@@ -45,7 +45,7 @@ export function CatchBoundary(
)
}
-export function ErrorComponent({ error }: { error: any }) {
+export function ErrorComponent({ error }: { error: unknown }) {
const [show, setShow] = Solid.createSignal(
process.env.NODE_ENV !== 'production',
)
@@ -81,7 +81,9 @@ export function ErrorComponent({ error }: { error: any }) {
overflow: 'auto',
}}
>
- {error.message ? {error.message} : null}
+ {(error as { message?: string } | null)?.message ? (
+ {(error as { message: string }).message}
+ ) : null}