UNPKG

@rsc-kit/mcp

Version:

An MCP server over what an rsc-kit build decided: the routes, why each one is static or not, and what it costs the browser.

153 lines (114 loc) • 5.5 kB
# 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' }`. :::