5.1 KiB
RulesError — Typed error registry for @suaveplan/rules
(no submodules)
Purpose
RulesError is the single error class thrown across the entire @suaveplan/rules surface. It extends SuaveplanError<RulesErrorCode> so every failure carries a stable, machine-readable code drawn from a frozen 24-entry registry, plus the standard SuaveplanError wire format, OTel-adapter integration, and cause-chain preservation. Callers should never catch a generic Error from this package — instanceof RulesError (or isRulesError(value)) is the canonical discrimination path.
The registry is defined at module load via defineCodes("RULES", [...] as const), which exports RULES_CODES as a readonly tuple literal. The RulesErrorCode type is derived from that tuple, so the TypeScript compiler enforces that every new RulesError(code, ...) call site uses a code that actually exists in the registry. Adding a new code requires an OpenSpec change (the conformance script verifies the registry against the spec's ## ADDED Requirements).
Features
- 24 stable codes covering engine / operator / field-resolution / action / event / validator / serializer / optimizer / plugin failure modes.
RulesErrorclass extendsSuaveplanErrorfor cross-package wire-format and OTel-adapter compatibility.- Optional
cause,traceId, anddetailsfields preserved through construction. isRulesError(value)type guard for catch arms that need to discriminate package errors from unrelated runtime errors.- Every code is forward-declared at module load — no dynamic registration, no string-typed escape hatches.
Usage: Basic Example
import { RulesError, isRulesError } from "@suaveplan/rules/errors";
function evaluate(rule: unknown) {
if (typeof rule !== "object" || rule === null) {
throw new RulesError("RULES.RULE_INVALID", "rule must be an object");
}
// ... real evaluation
}
try {
evaluate(null);
} catch (err) {
if (isRulesError(err) && err.code === "RULES.RULE_INVALID") {
// structured handling
} else {
throw err;
}
}
Usage: Advanced Example
import { RulesError } from "@suaveplan/rules/errors";
async function resolveField(data: unknown, path: string, strict: boolean) {
try {
return walk(data, path);
} catch (cause) {
throw new RulesError(
"RULES.FIELD_RESOLUTION_FAILED",
`cannot resolve "${path}" in strict mode`,
{
cause,
traceId: currentTraceId(),
details: { path, strict, hasData: data !== undefined },
},
);
}
}
The details object becomes part of the structured wire format when the error
crosses a process boundary (for example, when a Hono middleware serialises a
denied request into a JSON envelope). The OTel adapter on SuaveplanError
projects code, traceId, and details as span attributes automatically.
API Reference
RULES_CODES: readonly RulesErrorCode[]
The frozen 24-entry registry of stable error codes. Every entry is prefixed
with RULES. and corresponds to a documented failure mode in the package's
OpenSpec contract.
type RulesErrorCode
Literal-union type derived from RULES_CODES. Constructor calls that pass a
string not in the union are rejected at compile time.
class RulesError extends SuaveplanError<RulesErrorCode>
new RulesError(code, message, opts?)— constructor.code: RulesErrorCode— required, must be a member ofRULES_CODES.message: string— required, human-readable summary.opts.cause?: unknown— original thrown value when wrapping.opts.traceId?: string— propagated to the SuaveplanError base; appears in OTel span attributes.opts.details?: Readonly<Record<string, unknown>>— structured context; omitted fromerr.detailswhen not supplied.
- Inherits
code,message,cause,traceId,toWire()fromSuaveplanError. nameis"RulesError".
isRulesError(value: unknown): value is RulesError
Returns true only when value is a RulesError instance. Plain Error
objects with a code field do not satisfy the guard.
Implementation Notes
- The registry is intentionally flat (no nested namespaces). Sub-domain grouping is expressed via the prefix (
ENGINE_*,ACTION_*,VALIDATOR_*) rather than a structured tree, matching the convention used by@suaveplan/crypto-rsaand@suaveplan/webauthn. detailsis stored only when explicitly supplied; the property isundefined(not an empty object) otherwise. This keeps the wire format compact and lets receivers testif (err.details)without false positives.RulesErrorisextends SuaveplanErrornotextends Error— the AGENTS.md §2c rule against deleting registry entries to satisfy a gate applies here. If a code is "unused", the fix is to wire it into the path the proposal designed it for, not to remove it.- Cross-runtime: tests import runner primitives from
@suaveplan/testing/runnerper CLAUDE.md rule 11. Directvitest/bun:testimports are banned in this package.