Errors & redirects
error.tsx for when something throws, not-found.tsx for when nothing matched, and redirect.ts for a URL that moved. In this mode an unknown URL is a real 404, a redirect is answered with a 307 or 308, and a page that throws on the server still arrives with its frame, leaving error.tsx to the browser.
error.tsx
An error.tsx beside a layout.tsx (or a page.tsx) is what shows in place of what is below it when that throws — inside the layout, so the frame survives the failure. Catching a render error is something only the browser can do, so error.tsx is a client component.
// src/routes/error.tsx
'use client';
import type { ErrorProps } from '@k8ordo/router';
export default function RouteError({ error, reset }: ErrorProps) {
return (
<section role="alert">
<p>{error instanceof Error ? error.message : 'something went wrong'}</p>
<button onClick={reset} type="button">
try again
</button>
</section>
);
}It receives error, whatever was thrown, and reset, which renders the subtree again in place — and no params. The type is ErrorProps from @k8ordo/router, and the generated table checks each error.tsx with satisfies ErrorComponent. Wording that depends on the URL reads the URL itself.
What it covers
The boundary of an error.tsx sits inside the layout of its own directory. It covers the page.tsx beside it and everything in the directories below, their layouts included; when the layout beside it throws, the error.tsx one level up answers. The nearest one above the throw is the one that shows.
src/routes/
layout.tsx
error.tsx
page.tsx
shop/
layout.tsx
error.tsx
page.tsx
[id]/
page.tsxIn this tree shop/error.tsx catches a failure in shop/page.tsx or shop/[id]/page.tsx inside shop/layout.tsx, while a failure in shop/layout.tsx itself is caught by the root error.tsx, inside the root layout — as is a failure in the root page.tsx.
This site has two. src/routes/[locale]/error.tsx renders where the page was when a page throws, with the header and footer still around it; src/routes/error.tsx is the frameless full-screen last resort for when the [locale] layout itself throws.
reset renders the subtree again where it is. Navigating to another page clears the failure on its own, because the boundary is recreated for each navigation that puts a page on screen.
Try it
The button below mounts a client component that throws while rendering. This page's content is replaced by src/routes/[locale]/error.tsx while the header and footer stay; pressing "Retry" calls reset, and the page renders again in place.
When it throws during the server render
A server render has no error boundaries. What it has is the rule that a subtree which throws inside a Suspense boundary is left for the browser to render, and the boundary of an error.tsx is one. So the HTML arrives with the frame in place and a hole where the page was; the browser throws at the same spot, and error.tsx shows after hydration. The response status stays 200.
In production the error the browser receives carries React's generic message instead of the thrown one, which appears only in the server's log, as k8ordo: rendering /broken failed. The framework sets no digest, so error.digest is an empty string. What the visitor reads is whatever error.tsx says.
A throw with no Suspense boundary above it — an error.tsx is one — has nowhere to leave a hole, so the HTML cannot be produced, and serve() answers that request with a 500.
When no error.tsx catches it
When a page that arrived by client navigation fails to render and no error.tsx catches it, the framework loads the same URL as a document instead. A network failure, or an answer that is not a payload — a file the host serves — becomes a document load the same way. A failure during hydration is not reloaded: asking again for HTML that was already rendered cannot make it better, and would only loop.
In this mode the document load is serve()'s own answer — a 500 when the page cannot be rendered. A URL the application does not have is not a document load: the handler answers it with the not-found payload under a 404, and it renders in place.
not-found.tsx
A not-found.tsx is the catch-all (/*) for any pathname below its directory that nothing else answered. It comes last in its branch, so every declared route is tried first. Like a page it receives params and pathname, and renders its own <title>.
// src/routes/not-found.tsx
export default function NotFoundPage() {
return (
<>
<title>Not found</title>
<h1>This page does not exist</h1>
</>
);
}The parameters above a catch-all are not validated, so the params.locale a not-found.tsx at /:locale/* receives can be any string. Check it before using it.
In this mode not-found.tsx is answered under a genuine 404 status. Each directory may declare its own, so docs/not-found.tsx can render an unknown URL under /docs inside docs/layout.tsx. With none declared, an unknown pathname gets a minimal page — a 404 heading and one line — under a 404.
redirect.ts
A directory that has moved keeps a redirect.ts instead of a page.tsx. It default-exports the target: a string, or { to, permanent }.
// 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 pattern the matched params fill in, so /:locale/legacy can send to /:locale/new. A target naming a param its own pattern does not have fails when the redirect is answered.
A redirect is consulted before the table. A directory that redirects has no page to render, so one holding both page.tsx and redirect.ts fails the build — and since a redirect counts as a declared URL, another group putting a page at the same URL is refused too.
In this mode the answer is a 307, or a 308 when permanent, with the target in location. A client navigation that meets a redirect gets HTML back instead of a payload, hands the URL to the browser, and the browser follows the redirect as a document load, so the address bar ends up right. The shape of the default export is exported as RedirectTarget.
A Server Action sends the visitor elsewhere with redirect(), covered here. Actions & requests
The statuses the application answers with
A page never decides its status; which route answered, and how, does.
| When | Status |
|---|---|
| A page rendered | 200 |
A Server Component threw under an error.tsx (that part is left to the browser) | 200 |
| A pathname the table does not have, or a value a schema refused | 404 |
redirect.ts (308 when permanent) | 307 |
An action posted by a form without JavaScript called redirect() | 303 |
A POST with no Origin header, or one whose host does not match | 403 |
The handler could not produce an answer (under serve()) | 500 |