Guides: Handling errors

Guides

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:

src/routes/books/[id]/+page.server.ts
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:

src/routes/+error.tsx
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
statusThe HTTP status.
messageThe message given to error(); "Not Found" for an unknown URL; "Internal Server Error" for an unexpected error.
errorWhat was thrown. For unexpected errors it's null in production.
routeThe 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:

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 with use={enhance}, the error goes to its onError callback 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.

Edit this page