Skip to content

An error page is a full-page route rendered when a request can't be satisfied.

It is distinct from an error boundary, which catches a rendering error inside an otherwise-working page, and from api/errors.ts, which handles failures on the API side.

Today there is exactly one error page: 404. The pages/<code>.* shape is deliberate, so more can join it later without changing the convention.

The 404 page

Every frontend source folder gets one, at the root of pages/:

FrameworkFile
Reactpages/404.tsx
SolidJSpages/404.tsx
Vuepages/404.vue
Sveltepages/404.svelte
MDXpages/404.mdx

It is seeded once when the folder is created, carrying a placeholder you are expected to replace. From that point it is an ordinary source file: it is never regenerated, never overwritten by a later boilerplate pass, and - unlike route files - it cannot be seeded through custom templates. Edit it directly.

tsx
// pages/404.tsx
export default function NotFound() {
  return (
    <main>
      <h1>404 - Not Found</h1>
      <a href="/">Back home</a>
    </main>
  );
}

How it is registered

The generator appends it to the folder's route list as the router's catch-all (path: "*"), always last, so it matches only after every real route has failed to.

You never register it yourself and it never appears in the typed Link route map - there is no route name to link to.

Two consequences worth knowing:

  • The app file wraps it; layouts do not. The catch-all is a top-level sibling of your routes, so app.* - your global shell, nav, providers - still renders around it, but no layout.* does. A 404 under /dashboard/anything gets the app shell, not the dashboard layout.

  • It is lazy-loaded on the client like any other page, and imported eagerly into the SSR bundle so the server can render it without a dynamic import.

CSR, SSR and SSG

The same component is used in every mode - what differs is the HTTP status the visitor actually receives:

ModeRenders the pageHTTP status
CSR✅ client router matches the catch-allwhatever served the SPA fallback - normally 200
SSR✅ rendered on the server404
SSG❌ no 404.html is emittedyour static host decides

A client-rendered 404 is not a 404 to a crawler

Under CSR the host has already answered - typically 200 OK with index.html - before your router decides nothing matched. The visitor sees the right page; a crawler or an uptime check sees a success. If correct status codes matter (SEO, monitoring), either enable SSR for that folder, or configure the fallback at the host/proxy level to return 404 for unknown paths.

For SSG folders, nothing is pre-rendered for unmatched paths - static hosts have their own not-found configuration (404.html on GitHub Pages and Netlify, error_page in Nginx, and so on). Point it at whatever your host expects.

What the 404 page is not

  • Not for API 404s. A missing record behind /api/users/[id] is a backend concern: return a declared [404, "json", ...] response variant, or let it reach api/errors.ts. Requests under apiBase never render a page.
  • Not a render-error handler. If a page throws while rendering, that is an error boundary's job. The 404 page only ever renders because routing found nothing.
  • Not not-found.tsx. There is no per-route not-found convention as in Next's App Router - one 404 page serves the whole source folder. Different folders have their own, which is usually the distinction you actually wanted. Migration Tips&nbps;›

Triggering it deliberately

There is no notFound() helper. A route that exists but has nothing to show is a normal render decision - branch in the component and render your own not-found UI, or redirect. The catch-all page is reserved for URLs that match no route at all.

Released under the MIT License.