Errors

When something goes wrong, Goribu shows visitors a friendly page and keeps the details for you.

On this page

Expected and unexpected failures

Goribu treats errors in two ways:

  • Expected failures are normal responses you return yourself: res.invalid() for form errors, res.notFound() for a missing resource, or a status with JSON for an API.
  • Unexpected errors are anything thrown from a handler, middleware, metadata, or rendering. Goribu catches them, shows details in development, and shows your 500 page with an error ID in production.

Mistakes in routes or configuration don't reach visitors at all. They fail the build or dev server startup.

An API returns its expected error directly:

src/routes/api/greet.ts
import type { Handler } from "goribu/server";

export const POST: Handler = async (req, res) => {
  const { name } = await req.body() as Record<string, unknown>;
  if (typeof name !== "string" || !name.trim()) {
    return res.status(400).json({ error: "A name is required." });
  }
  return res.json({ name: name.trim() });
};
src/routes/api/greet.js
export const POST = async (req, res) => {
  const { name } = await req.body();
  if (typeof name !== "string" || !name.trim()) {
    return res.status(400).json({ error: "A name is required." });
  }
  return res.json({ name: name.trim() });
};

A 404 page

Add a custom page for unmatched URLs and res.notFound() responses:

src/routes/_404.tsx
import { Link } from "goribu";

export const meta = { title: "Page not found", robots: "noindex" };

// Help the visitor recover from a missing URL.
export default function NotFoundPage() {
  return (
    <main>
      <h1>Page not found</h1>
      <p>The page may have moved or no longer exists.</p>
      <Link href="/">Go home</Link>
    </main>
  );
}
src/routes/_404.jsx
import { Link } from "goribu";

export const meta = { title: "Page not found", robots: "noindex" };

// Help the visitor recover from a missing URL.
export default function NotFoundPage() {
  return (
    <main>
      <h1>Page not found</h1>
      <p>The page may have moved or no longer exists.</p>
      <Link href="/">Go home</Link>
    </main>
  );
}
  • When it shows: For unknown URLs and whenever a handler returns res.notFound(). Without _404, Goribu uses a built-in page.
  • Passing data: res.notFound(props) passes props to the page. Metadata comes from _404's own meta, not the calling route.
  • Static in production: Since v0.9.3, the build renders _404 once and serves it from Static Assets, so unknown URLs never invoke the Worker. See Which requests run the Worker.
  • No request data: That static copy is shared by every visitor, gets empty props and the URL /404, and has no responseHeaders. It is there to protect against repeated requests for irrelevant pages. res.notFound(props) and misses under a dynamic route still render on the Worker with the request.

Because props can be empty, a meta function must handle missing fields:

src/routes/_404.tsx
import type { Meta } from "goribu";

type Props = { post?: { title: string } };

export const meta: Meta<Props> = ({ props }) => ({
  title: props.post ? `${props.post.title} was removed` : "Page not found",
  robots: "noindex",
});
src/routes/_404.jsx
export const meta = ({ props }) => ({
  title: props.post ? `${props.post.title} was removed` : "Page not found",
  robots: "noindex",
});

The build stops with a message naming the fix when:

  • _404 throws while rendering without a request, including inside Suspense. Move request-specific content to a handler using res.notFound(props).
  • Another 404.html exists in public/ or from a static route named 404. Customize src/routes/_404.tsxsrc/routes/_404.jsx instead.
  • public/.assetsignore excludes 404.html.

A 500 page

A production error page receives url, errorId, and when it's actually behind Cloudflare a cfRay:

src/routes/_500.tsx
import type { ServerErrorProps } from "goribu/server";

export const meta = { title: "Something went wrong", robots: "noindex" };

// Share the reference ID without exposing the underlying exception.
export default function ServerErrorPage({ url, errorId, cfRay }: ServerErrorProps) {
  return (
    <main>
      <h1>Something went wrong</h1>
      <p>We could not load {url}.</p>
      <p>Reference: <code>{errorId}</code></p>
      {cfRay && <p>Ray ID: <code>{cfRay}</code></p>}
    </main>
  );
}
src/routes/_500.jsx
export const meta = { title: "Something went wrong", robots: "noindex" };

// Share the reference ID without exposing the underlying exception.
export default function ServerErrorPage({ url, errorId, cfRay }) {
  return (
    <main>
      <h1>Something went wrong</h1>
      <p>We could not load {url}.</p>
      <p>Reference: <code>{errorId}</code></p>
      {cfRay && <p>Ray ID: <code>{cfRay}</code></p>}
    </main>
  );
}
  • url: The pathname only, without the query string.
  • errorId: Logged on the server next to the original error, so a visitor can quote it and you can find the cause.
  • cfRay: Cloudflare's Ray ID for the request, also logged with the error. It's absent in local development, so render it conditionally.
  • Fallback: If _500 itself fails, Goribu returns plain text with the same IDs.
  • Keep it safe: Never show the caught exception in the page.

Debugging in development

  • Request errors: Return a plain-text 500 with the message and stack trace.
  • Import errors: Vite's overlay shows the original error and the importing module, including server-only imports reaching the browser. Fix it and the page reloads without restarting pnpm dev.
  • Malformed bodies: Invalid JSON or multipart data returns a safe 400, in development and production alike.

Debugging in production

Every unhandled error is logged with its error ID and Ray ID. There are two ways to find it:

  • Past errors: Turn on Workers Logs for your Worker in the Cloudflare dashboard, then search for the ID a visitor gives you. Check Cloudflare's pricing (starting from Dec. 20206) for log volume and retention.
  • Live errors: Stream logs while you reproduce the problem. This shows only requests from the moment it starts:
Terminal
pnpm exec goribu tail

Keep credentials and private request data out of your own log messages.

Common repairs

Message or symptomWhat to check
Route has no page or HTTP handlerMove a helper outside the route directory, or add a supported export.
Conflicting route shapesRemove or rename duplicate URLs, including differently named dynamic parameters.
Handler returned an invalid valueReturn a res.* result instead of a plain object or Response.
res.render() needs a default exportAdd the page or return JSON, text, or a redirect.
Body could not be parsedMatch the request's Content-Type and send valid JSON or multipart data.
D1 or KV binding is missingDeclare the resource in config and restart development or rebuild.
D1 statement was awaited without a terminalAdd .get(), .all(), .value(), or .run().
Server-only module reached browser codeImport it only from a handler or middleware.
Migration history driftedRestore applied files and add a new forward migration.
Forced prerender cannot completeRemove the incompatible handler or middleware, or use Worker rendering.
  • Read the whole message: [Goribu] messages name the file or API and the fix. The CLI omits stack traces and adds a Caused by: line when there's an underlying reason.
  • Wrangler: Run any suggested Wrangler command as pnpm exec goribu wrangler <args>.