Skip to main content

Error Boundaries

Error boundaries catch errors during route loading, rendering, and navigation, allowing you to display fallback UI instead of breaking your entire application.

Overview

TanStack Router provides built-in error boundary support at multiple levels:
  • Route-level error components
  • Router-level default error component
  • Granular error handling per route
  • Error recovery and retry mechanisms

Basic Error Handling

Route Error Component

Define error UI for individual routes:
src/routes/posts/$postId.tsx
import { createFileRoute, ErrorComponent } from '@tanstack/react-router'
import { fetchPost, NotFoundError } from '../../api'
import type { ErrorComponentProps } from '@tanstack/react-router'

export const Route = createFileRoute('/posts/$postId')({
  loader: async ({ params }) => {
    const post = await fetchPost(params.postId)
    return post
  },
  errorComponent: PostErrorComponent,
  component: PostComponent,
})

function PostErrorComponent({ error }: ErrorComponentProps) {
  if (error instanceof NotFoundError) {
    return (
      <div className="p-4">
        <h2>Post Not Found</h2>
        <p>The post you're looking for doesn't exist.</p>
      </div>
    )
  }
  
  // Fall back to default error component for other errors
  return <ErrorComponent error={error} />
}

function PostComponent() {
  const post = Route.useLoaderData()
  return <div>{post.title}</div>
}

Default Error Component

TanStack Router provides a default error component:
import { ErrorComponent } from '@tanstack/react-router'

export const Route = createFileRoute('/posts/$postId')({
  loader: ({ params }) => fetchPost(params.postId),
  errorComponent: ErrorComponent, // Use default
  component: PostComponent,
})

Router-Level Error Handling

Set a default error component for all routes:
src/main.tsx
import { createRouter } from '@tanstack/react-router'
import { DefaultCatchBoundary } from './components/DefaultCatchBoundary'

const router = createRouter({
  routeTree,
  defaultErrorComponent: DefaultCatchBoundary,
})
src/components/DefaultCatchBoundary.tsx
import { ErrorComponent } from '@tanstack/react-router'
import { Link } from '@tanstack/react-router'
import type { ErrorComponentProps } from '@tanstack/react-router'

export function DefaultCatchBoundary({ error }: ErrorComponentProps) {
  return (
    <div className="min-h-screen flex items-center justify-center">
      <div className="max-w-md w-full p-6 bg-red-50 rounded-lg">
        <h1 className="text-2xl font-bold text-red-900 mb-4">
          Something went wrong
        </h1>
        <ErrorComponent error={error} />
        <div className="mt-4">
          <Link to="/" className="text-blue-600 hover:underline">
            Go back home
          </Link>
        </div>
      </div>
    </div>
  )
}

Root Route Error Boundary

Catch errors at the application level:
src/routes/__root.tsx
import { createRootRoute, Outlet } from '@tanstack/react-router'
import { DefaultCatchBoundary } from '../components/DefaultCatchBoundary'
import { NotFound } from '../components/NotFound'

export const Route = createRootRoute({
  errorComponent: DefaultCatchBoundary,
  notFoundComponent: NotFound,
  component: RootComponent,
})

function RootComponent() {
  return (
    <>
      <Outlet />
    </>
  )
}

Custom Error Types

Create custom error classes for better error handling:
src/utils/errors.ts
export class NotFoundError extends Error {
  constructor(message: string) {
    super(message)
    this.name = 'NotFoundError'
  }
}

export class UnauthorizedError extends Error {
  constructor(message = 'Unauthorized') {
    super(message)
    this.name = 'UnauthorizedError'
  }
}

export class ValidationError extends Error {
  constructor(
    message: string,
    public fields: Record<string, string>,
  ) {
    super(message)
    this.name = 'ValidationError'
  }
}
Handle them in your error component:
import { NotFoundError, UnauthorizedError, ValidationError } from '../utils/errors'

function PostErrorComponent({ error, reset }: ErrorComponentProps) {
  if (error instanceof NotFoundError) {
    return (
      <div>
        <h2>Not Found</h2>
        <p>{error.message}</p>
      </div>
    )
  }
  
  if (error instanceof UnauthorizedError) {
    return (
      <div>
        <h2>Access Denied</h2>
        <p>You don't have permission to view this content.</p>
        <Link to="/login">Log in</Link>
      </div>
    )
  }
  
  if (error instanceof ValidationError) {
    return (
      <div>
        <h2>Validation Error</h2>
        <ul>
          {Object.entries(error.fields).map(([field, message]) => (
            <li key={field}>{field}: {message}</li>
          ))}
        </ul>
      </div>
    )
  }
  
  // Generic error
  return <ErrorComponent error={error} />
}

Error Recovery

The error component receives a reset function to retry:
import type { ErrorComponentProps } from '@tanstack/react-router'

function PostErrorComponent({ error, reset }: ErrorComponentProps) {
  return (
    <div className="p-4">
      <h2 className="text-xl font-bold text-red-600">Error Loading Post</h2>
      <p className="my-2">{error.message}</p>
      <button
        onClick={() => reset()}
        className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
      >
        Try Again
      </button>
    </div>
  )
}

Loader Errors

Errors thrown in loaders are caught by error boundaries:
export const Route = createFileRoute('/posts/$postId')({
  loader: async ({ params }) => {
    const response = await fetch(`/api/posts/${params.postId}`)
    
    if (!response.ok) {
      if (response.status === 404) {
        throw new NotFoundError(`Post ${params.postId} not found`)
      }
      if (response.status === 401) {
        throw new UnauthorizedError()
      }
      throw new Error('Failed to load post')
    }
    
    return response.json()
  },
  errorComponent: PostErrorComponent,
})

Not Found vs Error

Distinguish between 404 errors and other errors:
src/routes/posts/$postId.tsx
export const Route = createFileRoute('/posts/$postId')({
  loader: async ({ params }) => {
    const post = await fetchPost(params.postId)
    
    if (!post) {
      throw notFound() // Triggers notFoundComponent
    }
    
    return post
  },
  notFoundComponent: () => (
    <div>
      <h2>Post Not Found</h2>
      <Link to="/posts">View all posts</Link>
    </div>
  ),
  errorComponent: ({ error }) => (
    <div>
      <h2>Error Loading Post</h2>
      <p>{error.message}</p>
    </div>
  ),
})

Nested Error Boundaries

Error boundaries at different levels provide granular error handling:
src/routes/__root.tsx
export const Route = createRootRoute({
  errorComponent: RootErrorComponent, // Catches all unhandled errors
})
src/routes/posts.tsx
export const Route = createFileRoute('/posts')({
  errorComponent: PostsErrorComponent, // Catches errors in posts section
})
src/routes/posts/$postId.tsx
export const Route = createFileRoute('/posts/$postId')({
  errorComponent: PostErrorComponent, // Catches errors for specific post
})
Errors bubble up to the nearest error boundary.

Global Error Handling

Combine with window error handlers for comprehensive coverage:
src/main.tsx
import { useEffect } from 'react'

function App() {
  useEffect(() => {
    const handleError = (event: ErrorEvent) => {
      console.error('Global error:', event.error)
      // Log to error tracking service
      trackError(event.error)
    }
    
    const handleRejection = (event: PromiseRejectionEvent) => {
      console.error('Unhandled promise rejection:', event.reason)
      trackError(event.reason)
    }
    
    window.addEventListener('error', handleError)
    window.addEventListener('unhandledrejection', handleRejection)
    
    return () => {
      window.removeEventListener('error', handleError)
      window.removeEventListener('unhandledrejection', handleRejection)
    }
  }, [])
  
  return <RouterProvider router={router} />
}

Error Tracking Integration

Integrate with services like Sentry:
import * as Sentry from '@sentry/react'
import { ErrorComponent } from '@tanstack/react-router'
import type { ErrorComponentProps } from '@tanstack/react-router'

function CustomErrorComponent({ error, reset }: ErrorComponentProps) {
  useEffect(() => {
    // Report to Sentry
    Sentry.captureException(error)
  }, [error])
  
  return (
    <div>
      <h2>Something went wrong</h2>
      <ErrorComponent error={error} />
      <button onClick={reset}>Try Again</button>
    </div>
  )
}

Best Practices

Granular Boundaries

Use error boundaries at multiple levels for better UX

Custom Error Types

Create specific error classes for different scenarios

User-Friendly Messages

Show helpful error messages, not technical stack traces

Error Recovery

Provide retry buttons when appropriate
Development vs Production: Show detailed error info in development, but user-friendly messages in production.
Don’t catch everything: Let critical errors bubble up. Only catch errors you can meaningfully handle.

Testing Error Boundaries

Test your error handling:
// Trigger an error
export const Route = createFileRoute('/test-error')({
  loader: () => {
    throw new Error('Test error')
  },
  errorComponent: ({ error }) => <div>Caught: {error.message}</div>,
})

Next Steps

Route Guards

Prevent errors with validation before loading

SSR

Handle errors in server-side rendering

Build docs developers (and LLMs) love