Errors in production
Details of an unexpected error never reach the client in production.
Details of an unexpected error never reach the client in production. What the client sees:
| Thrown | Status | Message shown |
|---|---|---|
HttpError 4xx |
its status | its message (written for the caller) |
HttpError 5xx |
its status | the reason phrase; the message only with expose: true or in development |
| validation failure | 400 | Invalid input and the issues |
notFound() |
404 | Not Found |
| anything else | 500 | Internal Server Error; the detail only in development |
| a request that timed out | 504 | Gateway Timeout |
Unexpected errors and HttpErrors from 500 up go to onError(error, { phase, event }) exactly once, however many layers
the error passed. The phase is request, middleware, api, rpc, loader, render, stream or head, and
event.id is the id the response carries in x-request-id and, for JSON answers, in error.requestId, so a bug
report that quotes the id finds the log line. Without onError the handler prints
[dusk] <phase> error (request <id>, digest <digest>) and the error to console.error. 4xx errors are not reported.
The digest is the short code (8 hexadecimal characters) that ties a page to its log line. The public error of an
unexpected error carries it as error.digest (a route's error export and the global error page receive it), and the
log line prints the same value. It is a keyed hash of the error's root cause with a key that is random per process: the
message is never in it, and it cannot be checked against a guessed message from outside the process. The client gets
it through the hydration data. It is not a secret, and it changes when the process restarts. See
the error UI paragraph of the Dusk routing guide.
import { HttpError, type RequestEvent } from "@shadow-js/dusk/server";
export const GET = (event: RequestEvent): never => {
switch (event.url.searchParams.get("kind")) {
case "client":
throw new HttpError(403, "Not your document");
case "server":
throw new HttpError(502, "upstream answered with a stack trace");
case "exposed":
throw new HttpError(503, "Down for maintenance", { expose: true });
default:
throw new Error("password for the database is hunter2");
}
};