Invalid Search Parameters
-
{error.message}
+
{error instanceof Error ? error.message : String(error)}
router.navigate({ to: '/search', search: {} })}>
Reset Search
diff --git a/docs/start/framework/react/guide/server-components.md b/docs/start/framework/react/guide/server-components.md
index 893a6333394..829e78a3c76 100644
--- a/docs/start/framework/react/guide/server-components.md
+++ b/docs/start/framework/react/guide/server-components.md
@@ -943,7 +943,11 @@ export const Route = createFileRoute('/')({
// If this fails, the errorComponent renders
Greeting: await getGreeting(),
}),
- errorComponent: ({ error }) =>
Failed to load: {error.message}
,
+ errorComponent: ({ error }) => (
+
+ Failed to load: {error instanceof Error ? error.message : String(error)}
+
+ ),
component: HomePage,
})
```
diff --git a/packages/react-router/src/Match.tsx b/packages/react-router/src/Match.tsx
index 5de5546aeb8..544a5c3823f 100644
--- a/packages/react-router/src/Match.tsx
+++ b/packages/react-router/src/Match.tsx
@@ -378,7 +378,7 @@ export const MatchInner = React.memo(function MatchInnerImpl({
ErrorComponent
return (
{
+ expectTypeOf().toBeUnknown()
+})
+
+test('error-specific properties are accessible after narrowing', () => {
+ const narrowed = (props: ErrorComponentProps) =>
+ props.error instanceof Error ? props.error.message : String(props.error)
+
+ expectTypeOf(narrowed).returns.toEqualTypeOf()
+})
+
+test('error-specific properties are inaccessible without narrowing', () => {
+ const unnarrowed = (props: ErrorComponentProps) =>
+ // @ts-expect-error - `error` is `unknown`; narrow it before accessing `message`
+ props.error.message
+
+ expectTypeOf(unnarrowed).toBeFunction()
+})
diff --git a/packages/react-router/tests/errorComponent.test.tsx b/packages/react-router/tests/errorComponent.test.tsx
index 296c0caf348..0d19aa839be 100644
--- a/packages/react-router/tests/errorComponent.test.tsx
+++ b/packages/react-router/tests/errorComponent.test.tsx
@@ -23,7 +23,10 @@ import {
import type { ErrorComponentProps, RouterHistory } from '../src'
function MyErrorComponent(props: ErrorComponentProps) {
- return Error: {props.error.message}
+ const message =
+ props.error instanceof Error ? props.error.message : String(props.error)
+
+ return Error: {message}
}
async function asyncToThrowFn() {
@@ -380,7 +383,11 @@ 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: {error instanceof Error ? error.message : String(error)}
+
+ ),
})
const handler = createRequestHandler({
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..08cc51dd1d1 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,10 @@ function setup({ failVia }: { failVia: 'render' | 'loader' }) {
history: createMemoryHistory({ initialEntries: ['/test'] }),
defaultErrorComponent: (props: ErrorComponentProps) => {
errorRenders++
- return error: {props.error.message}
+ const message =
+ props.error instanceof Error ? props.error.message : String(props.error)
+
+ return error: {message}
},
})
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 869aeec76c3..b05f5bb7e05 100644
--- a/packages/react-router/tests/loaders.test.tsx
+++ b/packages/react-router/tests/loaders.test.tsx
@@ -895,9 +895,12 @@ test('reproducer for #6388 - rapid navigation between parameterized routes shoul
},
errorComponent: ({ error }) => {
errorComponentRenderCount(error)
+ const message = error instanceof Error ? error.message : ''
+ const name = error instanceof Error ? error.name : ''
+
return (
- Error Component: {error.message} | Name: {error.name}
+ Error Component: {message} | Name: {name}
)
},
diff --git a/packages/react-router/tests/router.test.tsx b/packages/react-router/tests/router.test.tsx
index cf3b1c11feb..77f686ca411 100644
--- a/packages/react-router/tests/router.test.tsx
+++ b/packages/react-router/tests/router.test.tsx
@@ -1853,7 +1853,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 }) => {
@@ -1873,7 +1873,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 }) => {
@@ -1888,7 +1888,9 @@ describe('search params in URL', () => {
await act(() => router.load())
expect(errorSpy).toBeInstanceOf(SearchParamError)
- expect(errorSpy?.cause).toBeInstanceOf(TestValidationError)
+ expect((errorSpy as SearchParamError).cause).toBeInstanceOf(
+ TestValidationError,
+ )
})
})
})
diff --git a/packages/router-core/src/route.ts b/packages/router-core/src/route.ts
index 2de7dc57f05..d0c352b9fd2 100644
--- a/packages/router-core/src/route.ts
+++ b/packages/router-core/src/route.ts
@@ -1607,8 +1607,8 @@ export type ErrorRouteProps = {
reset: () => void
}
-export type ErrorComponentProps = {
- error: TError
+export type ErrorComponentProps = {
+ error: unknown
info?: { componentStack: string }
reset: () => void
}
diff --git a/packages/solid-router/tests/errorComponent.test-d.tsx b/packages/solid-router/tests/errorComponent.test-d.tsx
new file mode 100644
index 00000000000..88572bbeee6
--- /dev/null
+++ b/packages/solid-router/tests/errorComponent.test-d.tsx
@@ -0,0 +1,21 @@
+import { expectTypeOf, test } from 'vitest'
+import type { ErrorComponentProps } from '../src'
+
+test('error prop is unknown', () => {
+ expectTypeOf().toBeUnknown()
+})
+
+test('error-specific properties are accessible after narrowing', () => {
+ const narrowed = (props: ErrorComponentProps) =>
+ props.error instanceof Error ? props.error.message : String(props.error)
+
+ expectTypeOf(narrowed).returns.toEqualTypeOf()
+})
+
+test('error-specific properties are inaccessible without narrowing', () => {
+ const unnarrowed = (props: ErrorComponentProps) =>
+ // @ts-expect-error - `error` is `unknown`; narrow it before accessing `message`
+ props.error.message
+
+ expectTypeOf(unnarrowed).toBeFunction()
+})
diff --git a/packages/solid-router/tests/errorComponent.test.tsx b/packages/solid-router/tests/errorComponent.test.tsx
index 4ee1abf3f1b..4a14a58e1aa 100644
--- a/packages/solid-router/tests/errorComponent.test.tsx
+++ b/packages/solid-router/tests/errorComponent.test.tsx
@@ -12,7 +12,10 @@ import {
import type { ErrorComponentProps } from '../src'
function MyErrorComponent(props: ErrorComponentProps) {
- return Error: {props.error.message}
+ const message = () =>
+ props.error instanceof Error ? props.error.message : String(props.error)
+
+ return Error: {message()}
}
async function asyncToThrowFn() {
diff --git a/packages/solid-router/tests/link.test.tsx b/packages/solid-router/tests/link.test.tsx
index d0fc7ae4697..a65bc55f5f3 100644
--- a/packages/solid-router/tests/link.test.tsx
+++ b/packages/solid-router/tests/link.test.tsx
@@ -3660,7 +3660,9 @@ describe('Link', () => {
test('when navigating from /invoices to ./invoiceId and the current route is /posts/$postId/details', async () => {
const rootRoute = createRootRoute({
- errorComponent: (err) => {err.error.message}
,
+ errorComponent: (err) => (
+ {err.error instanceof Error ? err.error.message : ''}
+ ),
})
const indexRoute = createRoute({
@@ -5629,7 +5631,9 @@ describe('search middleware', () => {
test('search middlewares work', async () => {
const rootRoute = createRootRoute({
- errorComponent: (error) => {error.error.stack}
,
+ errorComponent: (error) => (
+ {error.error instanceof Error ? error.error.stack : ''}
+ ),
validateSearch: (input) => {
return {
root: input.root as string | undefined,
diff --git a/packages/solid-router/tests/router.test.tsx b/packages/solid-router/tests/router.test.tsx
index 1ee017937a7..f6c07927357 100644
--- a/packages/solid-router/tests/router.test.tsx
+++ b/packages/solid-router/tests/router.test.tsx
@@ -1380,7 +1380,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 }) => {
@@ -1400,7 +1400,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 }) => {
@@ -1415,7 +1415,9 @@ describe('search params in URL', () => {
await router.load()
expect(errorSpy).toBeInstanceOf(SearchParamError)
- expect(errorSpy?.cause).toBeInstanceOf(TestValidationError)
+ expect((errorSpy as SearchParamError).cause).toBeInstanceOf(
+ TestValidationError,
+ )
})
})
})
diff --git a/packages/solid-router/tests/server/errorComponent.test.tsx b/packages/solid-router/tests/server/errorComponent.test.tsx
index f342aaae0d9..7d0ba5f649d 100644
--- a/packages/solid-router/tests/server/errorComponent.test.tsx
+++ b/packages/solid-router/tests/server/errorComponent.test.tsx
@@ -20,7 +20,9 @@ describe('errorComponent (server)', () => {
},
component: () => Index route
,
errorComponent: ({ error }) => (
- Route error: {error.message}
+
+ Route error: {error instanceof Error ? error.message : String(error)}
+
),
})
diff --git a/packages/vue-router/src/CatchBoundary.tsx b/packages/vue-router/src/CatchBoundary.tsx
index 5d75de8d6d1..d5b6fe639d7 100644
--- a/packages/vue-router/src/CatchBoundary.tsx
+++ b/packages/vue-router/src/CatchBoundary.tsx
@@ -1,11 +1,7 @@
import * as Vue from 'vue'
+import type { ErrorComponentProps } from '@tanstack/router-core'
import type { ErrorRouteComponent } from './route'
-interface ErrorComponentProps {
- error: Error
- reset: () => void
-}
-
type CatchBoundaryProps = {
getResetKey: () => number | string
children: Vue.VNode
@@ -109,7 +105,7 @@ export function CatchBoundary(props: CatchBoundaryProps) {
export const ErrorComponent = Vue.defineComponent({
name: 'ErrorComponent',
props: {
- error: Object,
+ error: null,
reset: Function,
},
setup(props) {
diff --git a/packages/vue-router/src/Matches.tsx b/packages/vue-router/src/Matches.tsx
index da8220db293..58646a13e74 100644
--- a/packages/vue-router/src/Matches.tsx
+++ b/packages/vue-router/src/Matches.tsx
@@ -89,7 +89,12 @@ const errorComponentFn: ErrorRouteComponentType = (
) => {
return Vue.h('div', { class: 'error' }, [
Vue.h('h1', null, 'Error'),
- Vue.h('p', null, props.error.message || String(props.error)),
+ Vue.h(
+ 'p',
+ null,
+ (props.error instanceof Error ? props.error.message : '') ||
+ String(props.error),
+ ),
Vue.h('button', { onClick: props.reset }, 'Try Again'),
])
}
diff --git a/packages/vue-router/tests/errorComponent.test-d.tsx b/packages/vue-router/tests/errorComponent.test-d.tsx
new file mode 100644
index 00000000000..88572bbeee6
--- /dev/null
+++ b/packages/vue-router/tests/errorComponent.test-d.tsx
@@ -0,0 +1,21 @@
+import { expectTypeOf, test } from 'vitest'
+import type { ErrorComponentProps } from '../src'
+
+test('error prop is unknown', () => {
+ expectTypeOf().toBeUnknown()
+})
+
+test('error-specific properties are accessible after narrowing', () => {
+ const narrowed = (props: ErrorComponentProps) =>
+ props.error instanceof Error ? props.error.message : String(props.error)
+
+ expectTypeOf(narrowed).returns.toEqualTypeOf()
+})
+
+test('error-specific properties are inaccessible without narrowing', () => {
+ const unnarrowed = (props: ErrorComponentProps) =>
+ // @ts-expect-error - `error` is `unknown`; narrow it before accessing `message`
+ props.error.message
+
+ expectTypeOf(unnarrowed).toBeFunction()
+})
diff --git a/packages/vue-router/tests/errorComponent.test.tsx b/packages/vue-router/tests/errorComponent.test.tsx
index f606040f173..a59fcb34ee7 100644
--- a/packages/vue-router/tests/errorComponent.test.tsx
+++ b/packages/vue-router/tests/errorComponent.test.tsx
@@ -12,7 +12,10 @@ import {
import type { ErrorComponentProps } from '../src'
function MyErrorComponent(props: ErrorComponentProps) {
- return Error: {props.error.message}
+ const message =
+ props.error instanceof Error ? props.error.message : String(props.error)
+
+ return Error: {message}
}
async function asyncToThrowFn() {
diff --git a/packages/vue-router/tests/link.test.tsx b/packages/vue-router/tests/link.test.tsx
index e047fe59933..d288b153151 100644
--- a/packages/vue-router/tests/link.test.tsx
+++ b/packages/vue-router/tests/link.test.tsx
@@ -3733,7 +3733,9 @@ describe('Link', () => {
test('when navigating from /invoices to ./invoiceId and the current route is /posts/$postId/details', async () => {
const rootRoute = createRootRoute({
- errorComponent: (err) => {err.error.message}
,
+ errorComponent: (err) => (
+ {err.error instanceof Error ? err.error.message : ''}
+ ),
})
const indexRoute = createRoute({
@@ -5800,7 +5802,9 @@ describe('search middleware', () => {
test('search middlewares work', async () => {
const rootRoute = createRootRoute({
- errorComponent: (error) => {error.error.stack}
,
+ errorComponent: (error) => (
+ {error.error instanceof Error ? error.error.stack : ''}
+ ),
validateSearch: (input) => {
return {
root: input.root as string | undefined,
diff --git a/packages/vue-router/tests/router.test.tsx b/packages/vue-router/tests/router.test.tsx
index 44a7acd43c9..b94f1e1169f 100644
--- a/packages/vue-router/tests/router.test.tsx
+++ b/packages/vue-router/tests/router.test.tsx
@@ -1382,7 +1382,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 }) => {
@@ -1402,7 +1402,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 }) => {
@@ -1417,7 +1417,9 @@ describe('search params in URL', () => {
await router.load()
expect(errorSpy).toBeInstanceOf(SearchParamError)
- expect(errorSpy?.cause).toBeInstanceOf(TestValidationError)
+ expect((errorSpy as SearchParamError).cause).toBeInstanceOf(
+ TestValidationError,
+ )
})
})
})