> ## Documentation Index
> Fetch the complete documentation index at: https://better-router.bansal.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Errors and not found

> Custom error pages, 404, HTTPException, and validation failures.

Uncaught errors and unknown URLs need product UI, not a stack dump. Add two special files at the routes root.

## 404

```tsx title="src/routes/_404.tsx" theme={null}
import { Link } from "better-router/react";

export default function NotFound() {
  return (
    <main>
      <h1>Not found</h1>
      <Link href="/">Back home</Link>
    </main>
  );
}
```

`_not-found.tsx` is an alias for the same slot. This page **does not** wrap in `_layout.tsx`, but it still uses `src/routes/_document.tsx`. Include whatever chrome you need here.

Without this file, unmatched routes return plain text `Not Found`.

## Uncaught errors

```tsx title="src/routes/_error.tsx" theme={null}
import { Link } from "better-router/react";

export default function ErrorPage({ error, status }: { error: string; status: number }) {
  return (
    <main>
      <h1>{status === 401 ? "Sign in required" : "Something broke"}</h1>
      {status >= 500 ? <pre>{error}</pre> : <p>{error}</p>}
      <Link href="/">Home</Link>
    </main>
  );
}
```

Also not wrapped in `_layout.tsx`. It still renders inside `_document.tsx`. SPA navigations that fail receive JSON `{ error }` instead of this page.

## Throwing vs redirecting

| Situation                   | Do this                                                                                                       |
| --------------------------- | ------------------------------------------------------------------------------------------------------------- |
| HTML page, user must log in | `return redirect("/login")` from the loader                                                                   |
| JSON API, user must log in  | `throw new HTTPException(401)`                                                                                |
| Record missing              | `return redirect("/projects")` or `throw new HTTPException(404)`                                              |
| Validation (API)            | `defineHandler.withValidator` → `400`                                                                         |
| Validation (form)           | throw `HTTPException(422, { message: JSON.stringify({ field: "Required" }) })` so `useForm` can read `errors` |

```ts theme={null}
import { HTTPException } from "hono/http-exception";
import { redirect } from "better-router";
import { getCookie } from "hono/cookie";
import type { LoaderArgs } from "better-router";

export async function loader({ c }: LoaderArgs) {
  if (!getCookie(c, "session")) return redirect("/login");
  return { ok: true };
}
```

`HTTPException(401)` is correct for `/api/*`. On a document request it renders `_error.tsx` with `{ status: 401 }` — not a login redirect.

## Default output

If you skip `_error.tsx`, document requests get `<pre>message</pre>` and JSON requests get `{ "error": "message" }` with the exception status.
