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:
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() });
};
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:
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>
);
}
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 ownmeta, not the calling route. - Static in production: Since v0.9.3, the build renders
_404once 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 noresponseHeaders. 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:
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",
});
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:
_404throws while rendering without a request, including insideSuspense. Move request-specific content to a handler usingres.notFound(props).- Another
404.htmlexists inpublic/or from a static route named404. Customizesrc/routes/_404.tsxsrc/routes/_404.jsxinstead. public/.assetsignoreexcludes404.html.
A 500 page
A production error page receives url, errorId, and when it's actually behind Cloudflare a cfRay:
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>
);
}
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
_500itself 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:
pnpm exec goribu tail
Keep credentials and private request data out of your own log messages.
Common repairs
| Message or symptom | What to check |
|---|---|
| Route has no page or HTTP handler | Move a helper outside the route directory, or add a supported export. |
| Conflicting route shapes | Remove or rename duplicate URLs, including differently named dynamic parameters. |
| Handler returned an invalid value | Return a res.* result instead of a plain object or Response. |
res.render() needs a default export | Add the page or return JSON, text, or a redirect. |
| Body could not be parsed | Match the request's Content-Type and send valid JSON or multipart data. |
| D1 or KV binding is missing | Declare the resource in config and restart development or rebuild. |
| D1 statement was awaited without a terminal | Add .get(), .all(), .value(), or .run(). |
| Server-only module reached browser code | Import it only from a handler or middleware. |
| Migration history drifted | Restore applied files and add a new forward migration. |
| Forced prerender cannot complete | Remove 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 aCaused by:line when there's an underlying reason. - Wrangler: Run any suggested Wrangler command as
pnpm exec goribu wrangler <args>.