Documentation

Routing

Errors and 404

Answer 404 from a VitNode route loader with notFound(), and configure the app's localized 404 and 500 pages, their action buttons and the development error overlay.

VitNode ships localized error screens that render inside your app's layout, so a visitor who hits a missing page or a crash keeps the header, the navigation and the theme. Plugins signal the problem from their loaders; the app decides how it looks.

Answer 404 from a loader

When the record a URL asks for does not exist, throw notFound() from @tanstack/react-router in load:

plugins/site-notes/src/pages/note-page.tsx
import { notFound } from '@tanstack/react-router'

const loadNote = async (queryClient: QueryClient, idParam: string) => {
  // ... read the note through the query cache, as in Add a route
  if (!note) throw notFound() 

  return note
}

The loader stops, the page renders the not-found screen, and the server answers with a 404 status, so search engines drop the URL. Add a route builds this page in full.

To show something other than the app's 404 page for one route, give its page module a notFound component:

plugins/site-notes/src/pages/note-page.tsx
const NoteNotFound = () => (
  <p className="container mx-auto p-4 leading-relaxed">This note does not exist.</p>
)

export const route = defineRoute({
  load: async ({ context, params }) =>
    await loadNote(context.queryClient, params.id),
  notFound: NoteNotFound, 
})

notFound loads with the page module, so it costs unvisited routes nothing. Without one, the route falls back to the router's defaultNotFoundComponent.

A URL that matches no route at all gets the same default 404 page. It still renders inside the main layout, because the main shell route matches every public URL.

Configure the app's error pages

The app sets both screens once, in getRouter in src/router.tsx. A new app already contains this:

apps/web/src/router.tsx
import { createRouter as createTanStackRouter } from '@tanstack/react-router'
import {
  ErrorActions,
  NotFound,
  RouteErrorPage,
} from '@vitnode/core/tanstack/layout'

export function getRouter() {
  const router = createTanStackRouter({
    // ...
    defaultNotFoundComponent: () => <NotFound actions={<ErrorActions />} />,
    defaultErrorComponent: ({ error }) => (
      <RouteErrorPage actions={<ErrorActions />} error={error} />
    ),
  })

  return router
}
ComponentImportShows
NotFound@vitnode/core/tanstack/layoutThe localized 404 page
RouteErrorPage@vitnode/core/tanstack/layoutError500Page, plus an overlay with the error's details in development only
Error500Page@vitnode/core/tanstack/layoutThe localized 500 page alone, with the strings from core.global.errors.500
ErrorActions@vitnode/core/tanstack/layoutA Go back button and a Back to home link

Keep RouteErrorPage as the error component. Passing Error500Page directly works too, but you lose the development overlay that shows what went wrong.

Change the action buttons

actions takes any elements. To add a link next to the default buttons, render both:

apps/web/src/router.tsx
import { buttonVariants } from '@vitnode/core/components/ui/button'
import { ErrorActions, NotFound } from '@vitnode/core/tanstack/layout'
import { HrefLink } from '@vitnode/core/tanstack/link'

// in createTanStackRouter({ ... })
defaultNotFoundComponent: () => (
  <NotFound
    actions={
      <>
        <ErrorActions />
        <HrefLink className={buttonVariants({ size: 'lg', variant: 'outline' })} href="/site-notes/notes">
          Browse notes
        </HrefLink>
      </>
    }
  />
),
Keep error pages in the app

Plugins throw notFound() or let errors surface from their loaders. The app owns the screens, so every plugin's errors look the same.