@k8ordo/framework

Errors and redirects

Handle a page that throws, a URL nothing matches, and a URL that has moved.

On this page

error.tsx

src/routes/error.tsx
'use client';
Only the browser can catch an error thrown while rendering

import type { ErrorProps } from '@k8ordo/framework';

export default function RouteError({ reset }: ErrorProps) {
Renders the failed part again in place
  return (
    <section role="alert">
      <p>Something went wrong.</p>
      <button onClick={reset} type="button">
        Try again
      </button>
    </section>
  );
}

An error.tsx beside a layout or a page renders in place of what is below it when that throws. Parts of the layout such as the header stay.

error.tsx is a Client Component. Its props are error, the thrown value, and reset, which renders that part again. It receives no params.

Navigating to another page clears what error.tsx shows, without calling reset.

Scope

text
src/routes/
  layout.tsx
  error.tsx
  page.tsx
  shop/
    layout.tsx
    error.tsx
    page.tsx
    [id]/
      page.tsx

The nearest error.tsx above the throw catches it. In this layout, shop/error.tsx catches an error in shop/page.tsx or shop/[id]/page.tsx.

An error.tsx sits inside the layout.tsx of its own directory, so an error in shop/layout.tsx itself is caught by the root error.tsx.

With no error.tsx to catch it, the browser loads a page that failed after a client navigation again, as a document at the same URL. Under static mode that load gets the page’s file from the host, and under server mode serve() answers it.

Playground

Error demo

This site’s own src/routes/[locale]/error.tsx catches a Client Component that throws while rendering.

Try it

  1. Press “Throw while rendering”. This page’s content is replaced by “Something went wrong”, while the header and the sidebar stay.
  2. Press “Retry”. reset is called, and this page renders again in place.

Static builds

Under static mode, a page whose Server Component throws during the build is not written. The error is logged beside the page’s URL, and the build ends with static build could not render /broken — see the error above, naming every page that failed. An error.tsx above it changes nothing.

A Client Component is different. With a Suspense boundary above it, the file is written with that part left empty. The error happens when the browser renders it, and error.tsx shows. An error.tsx is itself a Suspense boundary. With no boundary above it, the build stops as it does for a Server Component.

Server rendering

Under server mode, a server render cannot catch an error with error.tsx. A part that throws inside a Suspense boundary is left for the browser to render. The HTML is sent with the layouts and without the page.

The same component throws again in the browser, and error.tsx shows after hydration. The status stays 200. In production the error the browser receives carries React’s generic message. The original goes to the server’s log as k8ordo: rendering /broken failed.

An error with no Suspense boundary above it leaves no HTML to send, and serve() answers that request with a 500.

not-found.tsx

src/routes/not-found.tsx
export default function NotFoundPage() {
  return (
    <>
      <title>Not found</title>
      <h1>This page does not exist</h1>
    </>
  );
}

A not-found.tsx answers any URL below its directory that no route matched. It comes last for its directory in the route table, so every declared route is tried first. It receives params and pathname and renders its own <title>. Its params stay strings.

Under static mode, not-found.tsx is written as one file, 404.html. A static host returns one file for every URL that does not exist, so there can be only one not-found.tsx. Under a locale segment is fine. Two or more stop the build with an error that begins a static host answers every unknown URL from one file. With none, no 404.html is written, and the static host decides what a URL that does not exist gets. How 404.html renders in the browser is covered in Deployment.

Under server mode, a not-found.tsx answers under a 404 status. Each directory may have its own, so docs/not-found.tsx answers a URL under /docs that does not exist, inside docs/layout.tsx. With none at all, a minimal page of the framework’s own, a 404 heading and one line, renders inside the root layout under a 404. The <html lang> and the stylesheets stay.

notFound()

src/routes/products/[id]/page.tsx
import { notFound } from '@k8ordo/framework';
import type { PageProps } from '@k8ordo/framework';

import { findProduct } from '../../../lib/catalog.server';

export default async function ProductPage({
  params,
}: PageProps<'/products/:id'>) {
  const product = await findProduct(params.id);
  if (product === undefined) notFound();
Throws here, so the next line never runs
  return <h1>{product.name}</h1>;
}

A schema decides only the shape of a parameter. Only the page knows whether the data exists, so when it does not, the page calls notFound(). In place of the page, the nearest not-found.tsx above renders under a 404, inside the layouts above it.

Under static mode, notFound() gives a 404 from anywhere in the page. A URL passed through paths whose page calls it stops the build, naming the URL: it would write a 404 page at a URL the site claims to have.

In a fallback.tsx shell, a notFound() called in the browser gives no 404: the nearest not-found.tsx renders in place, and the status stays 200. More on this in Parameters.

Under server mode, a document is not sent until the page’s own component has returned: the status goes out before the body, so it waits for a possible notFound(). A client navigation streams from the start without waiting for the page. When the page calls notFound() there, the browser loads the same URL again as a document, and the server answers it with the 404.

Warning

Pitfall

Under server mode, once the page has returned and the response has started, calling notFound() from under a <Suspense> no longer gives a 404. The nearest error.tsx catches it like any other error.

redirect.ts

src/routes/old/redirect.ts
export default '/products';
src/routes/[locale]/legacy/redirect.ts
export default { to: '/:locale/new', permanent: true };
The target is a route pattern, and `:locale` is filled from the matched URL

When a URL moves, its old directory holds a redirect.ts instead of a page.tsx. The default export is the target: a string, or { to, permanent }. A directory holding both page.tsx and redirect.ts stops the build with "old" cannot both render page.tsx and redirect — keep one.

Under static mode, a redirect is written as a page that sends the visitor on: HTML with a <meta http-equiv="refresh"> and a link, the same whether or not it is permanent. No index.rsc is written beside it. On a client navigation, the browser loads that page and then moves on to the target.

Under server mode, a GET or HEAD gets a 307, or a 308 when permanent, and any other method a 405. A client navigation that meets a redirect has the browser load it as a document and follow it. To send the visitor elsewhere at the end of a Server Action, use redirect(), covered in Server Actions.

Information

Note

The default export’s type is RedirectTarget from @k8ordo/framework/server. Write export default '/products' satisfies RedirectTarget to check the shape where it is written. Static mode refuses @k8ordo/framework/server, but an import type of it passes a static build.

k8ordo

React libraries that use Baseline features without holding back.

© 2026 k8o — MIT License

Typeset in Noto Sans JP & M PLUS 2