Reference

Error Pages

Goribu has two app-level error pages: src/routes/_404.jsx for unmatched URLs and src/routes/_500.jsx for uncaught production errors. Both are optional as Goribu ships minimal fallbacks that depend on neither your stylesheet nor any framework component, so they render even when something upstream is broken. Both must default-export a component, must not export handlers (the framework renders them, it doesn't route to them) and live only at the top level of src/routes/ as nested error pages are not supported.

A custom 404

Default-export a component. It receives url which is the unmatched pathname plus query string:

import { Link } from "goribu";

export const meta = { title: "404 · My App" };

export default function NotFound({ url }) {
  return (
    <main>
      <h1>Nothing here</h1>
      <p>Couldn't find <code>{url}</code>.</p>
      <Link href="/">Back to home</Link>
    </main>
  );
}

<Link>, <Form>, your stylesheet and a meta export all work as in any other route.

A custom 500

export const meta = { title: "500 · My App" };

export default function ServerError({ url, errorId }) {
  return (
    <main>
      <h1>Something went wrong</h1>
      <p>We hit an error loading <code>{url}</code>. Try reloading.</p>
      <p>If this keeps happening, send us this code: <code>{errorId}</code>.</p>
    </main>
  );
}

It receives url and errorId, an 8-character hex id for the failure. The same id is logged server-side as [goribu] error <id>: followed by the error, so a user-reported code maps straight to a line in wrangler tail. The component deliberately does not receive the thrown error: stack traces, SQL fragments, secrets and internal paths must never flow into rendered HTML. It is already logged under errorId.

A 404 for a missing record

There are two kinds of 404. A route 404 when no file matched the URL and is handled for you, when _404.jsx renders automatically. A resource 404, when the route matched but the record is missing and this is yours to signal, with res.notFound():

export async function GET(req, res) {
  const poll = await req.d1.get("SELECT * FROM polls WHERE id = ?", [req.params.id]);
  if (!poll) return res.notFound();
  return res.render(PollPage, { poll });
}

res.notFound(props?) renders the registered _404.jsx with a 404 status and because it renders the real _404 route the page hydrates normally and you can pass props for custom missing-resource copy. Setting status(404) on an ordinary render does not trigger _404.jsx and rendering a one-off component for a pattern the client doesn't know, ships non-hydrating terminal HTML, which is why res.notFound() is the right tool.

Going deeper

In development

goribu dev swaps _500.jsx for a built-in overlay showing the error name and message, the URL, a source snippet around the throwing line and the full stack trace. It is tree-shaken out of production builds, which always use _500.jsx or the default.

What bypasses _500.jsx

A few responses never reach the page:

  • 400 for a malformed JSON body — returned as plain text by the body parser before your handler runs.
  • 405 for an unsupported method — plain text with an Allow header.
  • An error inside _500.jsx itself — the framework logs [goribu] error page rendering failed: and returns a plain-text Internal Server Error. The bare fallback is intentional: it has to work when nothing else does.

Middleware does not run on error pages

Both pages bypass the middleware chain. If the auth or logging middleware was what threw, running it again to render the error page would loop, and letting per-folder middleware fire on every missed URL gets expensive. If an error page needs request-aware logic, do it in the component. It has url, which is usually enough.

SPA navigation

404s are SPA-aware: a client navigation to an unknown URL gets a JSON payload, the _404 module loads and history updates without a reload as the app is healthy, there is just no route there. 500s force a full HTML response even on a SPA navigation, because injecting an error page into a client that may already be in a bad state is worse than starting fresh.