Error boundaries
Catch render failures with +error.tsx boundaries, errorBoundary(), and wrapError().
A render failure in a page or layout does not take down the app. The nearest error boundary catches it and renders a fallback view.
Throw from a page
Throw RouteError with error() (or redirect() for navigation):
import { const error: (status: number, message: string) => nevererror } from "@ilha/router";
export default async function function User({ params }: {
params: {
id: string;
};
}): Promise<View>
User({ params: {
id: string;
}
params }: { params: {
id: string;
}
params: { id: stringid: string } }) {
const const user: {
name: string;
} | null
user = await function findUser(_id: string): Promise<{
name: string;
} | null>
findUser(params: {
id: string;
}
params.id: stringid);
if (!const user: {
name: string;
} | null
user) function error(status: number, message: string): nevererror(404, "No such user");
return <JSX.IntrinsicElements.h1: HTMLAttributes<HTMLHeadingElement>h1>{const user: {
name: string;
}
user.name: stringname}</JSX.IntrinsicElements.h1: HTMLAttributes<HTMLHeadingElement>h1>;
}
async function function findUser(_id: string): Promise<{
name: string;
} | null>
findUser(_id: string_id: string) {
return null as null | { name: stringname: string };
}
error(status, message) throws a RouteError; redirect(to, status?) throws a Redirect that the router converts into a client navigation.
+error.tsx files
Drop a +error.tsx next to a page (or in a parent folder) to catch failures for everything under that folder. The nearest boundary wins:
src/pages/
+error.tsx ← catches all routes
user/
+error.tsx ← catches /user/*
[id].server.tsx
// src/pages/user/+error.tsx
import type { AppError } from "@ilha/router";
import type { type View = VNode | UnsafeHtml | JsonText | AtomHandle<unknown> | Stream<View, unknown, unknown> | Component | View[] | Iterable<View>View } from "ilha";
export default function function ErrorPage({ error, children, }: {
error: AppError;
children?: View;
}): View
ErrorPage({
error: AppErrorerror,
children: Viewchildren,
}: {
error: AppErrorerror: AppError;
children?: Viewchildren?: type View = VNode | UnsafeHtml | JsonText | AtomHandle<unknown> | Stream<View, unknown, unknown> | Component | View[] | Iterable<View>View;
}) {
return (
<JSX.IntrinsicElements.div: HTMLAttributes<HTMLDivElement>div class?: Attrclass="card bg-base-100 shadow">
<JSX.IntrinsicElements.div: HTMLAttributes<HTMLDivElement>div class?: Attrclass="card-body">
<JSX.IntrinsicElements.h2: HTMLAttributes<HTMLHeadingElement>h2 class?: Attrclass="card-title">{error: AppErrorerror.AppError.status?: number | undefinedstatus ?? 500}</JSX.IntrinsicElements.h2: HTMLAttributes<HTMLHeadingElement>h2>
<JSX.IntrinsicElements.p: HTMLAttributes<HTMLParagraphElement>p>{error: AppErrorerror.AppError.message: stringmessage}</JSX.IntrinsicElements.p: HTMLAttributes<HTMLParagraphElement>p>
{children: Viewchildren}
</JSX.IntrinsicElements.div: HTMLAttributes<HTMLDivElement>div>
</JSX.IntrinsicElements.div: HTMLAttributes<HTMLDivElement>div>
);
}
Boundary components receive the error and the failed route as children.
errorBoundary() on the builder
With a hand-built router, register a boundary per pattern:
function router(options?: RouterOptions): RouterBuilderrouter()
.RouterBuilder.route: (pattern: string, page: Page, opts?: {
layouts?: LayoutHandler[];
}) => RouterBuilder
route("/user/:id", const UserPage: () => Promise<never>UserPage)
.RouterBuilder.errorBoundary: (pattern: string, handler: ErrorHandler) => RouterBuildererrorBoundary("/user/:id", (err: AppErrorerr) => function h(type: string | Fragment | ComponentFn | JsxComponent, props: PropBag | null | undefined, ...rest: View[]): VNodeh("p", null, `Oops: ${err: AppErrorerr.AppError.message: stringmessage}`));
The handler receives an AppError (message, status?) and a route snapshot, and returns a view or a component.
wrapError()
Wrap a single page when you want the fallback inline instead of a boundary file:
export default function wrapError(handler: ErrorHandler, page: Page): PagewrapError(({ message: stringmessage }) => function h(type: string | Fragment | ComponentFn | JsxComponent, props: PropBag | null | undefined, ...rest: View[]): VNodeh("p", null, message: stringmessage), const UserPage: () => Promise<never>UserPage);
wrapError catches everything the page throws — RouteError or any exception — and calls your handler with { message, status? }.
Related
| Topic | Guide |
|---|---|
| Throwing redirects | Routes and navigation |
| Server pages and frames | Server islands |
| Hardening and status codes | Middleware and security |