Handling errors
Two kinds of errors reach a visitor. Expected ones, like a book that doesn't exist or a page they may not see, you raise yourself with error(). Unexpected ones are bugs: an exception in a load function or a component. Grimoire renders both through your +error.tsx.
Expected errors
Throw error(status, message) from a load function, an action, or canMatch:
import { error, type LoadContext } from "@sigil-dev/grimoire";
import { getBook } from "../../../lib/server/db";
export function load({ params }: LoadContext) {
const book = getBook(Number(params.id));
if (!book) throw error(404, "No book with that id.");
return { book };
}The response gets that status, and the message is shown to the visitor, so write it for them.
An error page
src/routes/+error.tsx renders errors in your own design:
import { Head } from "@sigil-dev/grimoire";
interface Props {
status: number;
message: string;
}
const ErrorPage = ({ status, message }: Props) => (
<main class="error">
<Head>
<title>{status === 404 ? "Not found" : "Something went wrong"}</title>
</Head>
<h1>{status}</h1>
<p>{message}</p>
<p>
<a href="/">Back to the start</a>
</p>
</main>
);
export default ErrorPage;It receives:
| Prop | |
|---|---|
status | The HTTP status. |
message | The message given to error(); "Not Found" for an unknown URL; "Internal Server Error" for an unexpected error. |
error | What was thrown. For unexpected errors it's null in production. |
route | The requested path. |
Grimoire uses the nearest +error.tsx above the URL: for /admin/users it tries src/routes/admin/users/+error.tsx, then src/routes/admin/+error.tsx, then src/routes/+error.tsx. Folders with a parameter in their name ([id]) are skipped, so put error pages in plain folders.
When a page's load throws error(), the error page renders inside the layouts whose loads had already finished, so the site's header and navigation stay. For an unknown URL or an unexpected error, it renders on its own.
Unexpected errors
An exception that isn't an error(), a bug in a load or a component, becomes a 500. The visitor sees your error page with the message "Internal Server Error"; the exception itself is logged, and never sent to the browser in production.
Log it wherever you collect errors with handleError in hooks.server.ts:
import type { HandleError } from "@sigil-dev/grimoire/hooks";
export const handleError: HandleError = ({ error, event, status }) => {
console.error(`${status} on ${event.request.method} ${event.url.pathname}`, error);
};handleError runs for unexpected errors only, not for error(). Without any +error.tsx, an error.html in the project root is served for them instead, and failing that, plain text.
Errors in forms and navigation
- An action that throws
error()renders the error page for a normal form submission. For a form withuse={enhance}, the error goes to itsonErrorcallback instead, and the page stays (Form actions). - For validation problems, return
fail()rather than throwing: the page renders again with what the visitor typed (Forms and actions). - When client-side navigation lands on an error, the router loads the error page as a full page load.