@lunora/errors
The unified error layer — LunoraError, the error catalog, isLunoraError, invariant/unreachable, and the CLI renderer.
@lunora/errors is the single source of truth for how Lunora throws and surfaces
errors. It provides one LunoraError base (built on @visulima/error) plus a
central catalog mapping each machine code to its transport status, a human
title, and — where useful — an actionable Markdown hint and docsUrl. Those
hints render consistently in the CLI, the Vite error overlay, the Studio UI, and
the client SDK.
import { LunoraError, isLunoraError } from "@lunora/errors";
throw new LunoraError("NOT_FOUND", `no message with id ${id}`);
if (isLunoraError(error)) {
// error.code, error.status, error.hint, error.data are all typed
}The catalog
ERROR_CATALOG keys the well-known codes (BAD_REQUEST, FORBIDDEN,
NOT_FOUND, CONFLICT, NOT_UNIQUE, VALIDATION_ERROR, TOO_MANY_REQUESTS,
RLS_REQUIRED, INTERNAL, …) to { status, title, hint?, docsUrl? }. Passing a
code to new LunoraError(code) fills those defaults in; explicit options
override them.
Structural matching
isLunoraError(error) recognizes any error carrying the transport shape (a
string code + numeric status) — including errors rebuilt from the wire, where
instanceof can't be trusted. The runtime and Durable Object mappers use it to
choose the response status.
Invariants
invariant(condition, message) and unreachable(message) throw an INTERNAL
LunoraError, so even "should never happen" assertions participate in the layer
(redacted to a generic message on the wire, rich in logs).
Rendering
LunoraError mirrors @visulima/error's error model (type: "VisulimaError"
plus hint/title/loc), so @visulima/error's renderError renders it — hint
and all — directly. The CLI does this in @lunora/cli's renderLunoraError util;
@lunora/errors itself is zero-dependency so its class/catalog tree-shakes
cleanly into browser and workerd bundles.