# Errors and 404s

> What a visitor sees when a page throws, or asks for a url nothing answers.

Two files, both optional, both following the same rule as `loading.tsx`: the
nearest one wins.

## When a page throws

With nothing of your own, a page that throws shows the engine's error page
where the page was, and the layouts around it stay: in development the
message and stack, in production "Something went wrong" with the digest to
search the server log for, and a **Try again** in both. It used to show
nothing — React unmounted the document on hydration, a black page with the
cause nowhere near it.

That page is the fallback. To show something of your own:

Put an `error.tsx` in the directory you want to cover:

```tsx title="src/app/orders/error.tsx"
'use client'

import type { RouteErrorProps } from '@rsc-kit/core/RouteErrorBoundary'

export default function OrdersError({ error, reset }: RouteErrorProps) {
  return (
    <section>
      <h1>That did not work</h1>
      <p>{error.message}</p>
      <button type="button" onClick={reset}>Try again</button>
    </section>
  )
}
```

Anything below it that throws renders this instead. The layouts above stay on
screen — only the segment that failed is replaced.

`reset()` renders the segment again, for a failure that might not happen twice.
Navigating away clears it on its own.

:::caution[It has to be a client component]
`'use client'` at the top, or the build refuses it. Catching a render error is
a class component's job, it runs in the browser, and `reset` is a callback the
browser calls — none of which a server component can do.
:::

### What `error.message` says in production

React replaces it with a generic sentence and puts a hash on `error.digest`.
That is React, not this package: the real message could name a table, a query or
a path, and none of that should reach a browser.

Log the digest where you log the error, and the two line up.

### It does not catch everything

- **Errors in the layout above it.** The boundary sits inside that layout, so a
  layout that throws needs an `error.tsx` a directory up. The engine's own
  page is outermost and catches those too.
- **The build.** A page that throws every time it renders fails the build
  rather than shipping a stored error page. The boundary is for a request that
  goes wrong, not a page that is broken.

### Telling a refusal apart in a server component

A failed `rpc()` throws the engine's own error for a refusal, so a server
component can answer it rather than fail:

```tsx
import { ServerAuthenticationError } from '@rsc-kit/core/errors'

async function Invite({ token }: { token: string }) {
  try {
    return <Accept invite={await rpc('Invites.find', token)} />
  } catch (error) {
    if (error instanceof ServerAuthenticationError) return <SignInToAccept />
    throw error
  }
}
```

`ServerAuthenticationError` is a 401 from the backend, `ServerAuthorizationError`
a 403. Rethrow what you do not handle, so a not-found or a redirect still does
what it should.

## When nothing answers the url

`src/app/not-found.tsx` is rendered for any url no route matches — and a
client-side navigation to one renders it in place, layout kept, url changed,
status still 404, the way Next.js does once the page is overridden. Without
the file, a navigation to a missing url is a full document load of a plain
"Not found":

```tsx title="src/app/not-found.tsx"
import Link from '@rsc-kit/core/Link'

export default function NotFound() {
  return (
    <main>
      <h1>No such page</h1>
      <p>Nothing answers that url. <Link href="/">Go home</Link>.</p>
    </main>
  )
}
```

It renders through your root layout like any other page, and is served with a
real **404** — a page that says "not found" under a `200` is a page search
engines index.

An ordinary server component, so it can be `async` and read whatever it likes.
Without one, an unmatched url gets a plain `Not found` string.

### When the page has to decide

A url can match a route and still name nothing — `/posts/42` is a real route
and there may be no post 42. Only the page can know that, so the page says so:

```tsx title="src/app/posts/[slug]/page.tsx"
import { notFound } from '@rsc-kit/core/not-found'

export default async function PostPage({ params }) {
  const post = await findPost((await params).slug)

  if (!post) notFound()

  return <article>{post.title}</article>
}
```

Same page, same **404**. It throws, so nothing after it runs — and if you wrap
the call in a `try`/`catch`, rethrow what you do not recognise, or a missing
page becomes a blank region.

:::caution[Where you call it decides the status]
Called above every Suspense boundary — at the top of a page, in a layout —
nothing has been written yet, so the response is a real 404.

Called deeper, inside a boundary, the shell has already gone out with the
status line on it. The boundary shows its fallback instead and the status stays
`200`. If the status matters — and for anything a crawler or a cache sees, it
does — do the lookup above the boundary.

A `loading.tsx` is a boundary around its page, so a page under one is always
"deeper". When the answer depends on the request — who is asking, a record
their account can see — make the check in a `middleware.ts` beside the page.
It runs before anything is sent, on every path, including a page the build
stored, and its `notFound()` is a real 404. Its argument is the route's
params, as a plain object: `{ slug: '42' }`.
:::
