Errors
createError makes an error that carries a status. It is the same function in silgi/app and silgi/server.
createError({ status = 500, message, data, fatal, cause })
A status outside 400 to 599 becomes 500. A string is also accepted and becomes the message. cause is kept as the error's cause. Without a message, the message is the status text. isSilgiError(value) tells a Silgi error from any other.
Creating an error
createError({ status: 404, statusText: "Not Found", message, data }) makes the error. statusText is the status line text and defaults to the text of status; message defaults to it. data can be typed with createError<{ field: string }>(...). Nuxt's statusCode and statusMessage still work as deprecated names for status and statusText, so throw createError({ statusCode: 404 }) answers 404; in dev the first use of each logs a warning.
Where errors come from
An error thrown in a load, in route middleware or while rendering on the server shows the error page with that status. An error thrown in a handler route answers JSON { status, message, data }, or the HTML error page when the request prefers text/html. A handler called from a page or a form answers the caller with the error, as Handlers says.
A request that matches nothing is a 404: the HTML error page when the request prefers text/html, and JSON otherwise.
A path that has server routes, but none for the request's method, answers 405 with an allow header that lists the methods; see Server.
Any other error, one that is not a createError, is logged. If it has an HTTP error status or statusCode (400 to 599), the answer keeps that status; otherwise it is a 500. In production the message is the status text, such as "Internal Server Error" or "Conflict", and data is dropped. This holds for the HTML of a rendered page and for the payload the browser reads. In development the real message and the stack are shown. An error thrown after a streamed body has started cannot change the status.
The error page
app/error.tsx is the error page. It renders for every error, without a layout. Read the error with useError(), which every library provides, and leave the page with clearError.
import { clearError, useError } from "@silgi/react";
export default function ErrorPage() {
const error = useError();
return (
<main>
<h1>{error?.status}</h1>
<p>{error?.message}</p>
<button type="button" onClick={() => void clearError({ redirect: "/" })}>
Back home
</button>
</main>
);
}
The error has status, message, data and, in development only, stack. clearError() renders the current URL again; clearError({ redirect }) navigates to another URL. Without app/error.tsx each library shows a built-in page with the status, the message and a Back home link.
A failing navigation in the browser shows the error page at the new URL: middleware that throws, a load that answers an error, or an unknown layout. A page chunk that fails to load, as after a deploy, is a full page load instead. Any successful navigation clears the error.
Errors in the browser
An error thrown while the browser renders, which no ErrorBoundary catches and which reaches the library's root, shows the error page at the URL being rendered. Some libraries keep the page after an error that is not part of a render, such as Vue's error in an event handler or React's recoverable error; there the error page shows only for a createError({ fatal: true }).
The page:error app hook gets the error before the page renders. The app:error hook gets every error in the browser that reaches the navigator or the library's root, whether the error page shows or not. See Modules for app plugins.
Showing an error from the browser
showError(error) renders the error page at the current URL without changing the history, whatever fatal says. Use it for failures that no navigation caused. On the server, and while a navigation renders its page, it throws the error instead, so the error page answers for that URL.
import { createError, showError } from "silgi/app";
export default function Delete() {
const remove = async () => {
const response = await fetch("/api/account", { method: "DELETE" });
if (!response.ok) showError(createError({ status: response.status, message: "Could not delete the account" }));
};
return (
<button type="button" onClick={() => void remove()}>
Delete account
</button>
);
}
Error boundaries
ErrorBoundary catches errors thrown while its children render, in the browser and on the server, and renders its fallback, which receives { error, reset }. reset renders the children again.
import type { ReactNode } from "react";
import { ErrorBoundary } from "@silgi/react";
export default function Widget({ children }: { children?: ReactNode }) {
return (
<ErrorBoundary
fallback={({ error, reset }) => (
<div>
<p>{error instanceof Error ? error.message : "Something went wrong"}</p>
<button type="button" onClick={reset}>Try again</button>
</div>
)}
>
{children}
</ErrorBoundary>
);
}
The props of ErrorBoundary follow the library. In Vue the fallback is a #fallback slot with error and reset, or a fallback function prop. In Svelte it is a fallback snippet that gets { error, reset }. In html both children and fallback are functions that return a string. See Adapters.
On the server, a throwing subtree renders the fallback into the HTML, the page answers 200, and the browser hydrates in the fallback without a mismatch. reset does nothing on the server. This holds in all six libraries, with one limit:
- React: when the page streams and a suspended child fails after the first chunk was sent, the browser hydrates into the server's fallback. An
ErrorBoundarynested inside that failedSuspenseis rendered again in the browser.