Errors
CrustError codes, structured details, and error-boundary behavior.
CrustError
Framework definition, routing, parsing, and validation failures use CrustError. Branch on code or .is(code), never on the message.
import { Crust, CrustError } from "@crustjs/core";
import { z } from "zod";
const app = new Crust("serve")
.flags({ name: "port", type: "string", schema: z.coerce.number().int().min(1) })
.action(({ flags, stdout }) => stdout(`listening on ${flags.port}`));
const outcome = await app.run([], { flags: { port: "0" } });
if (outcome.status === "failed") {
const { error } = outcome;
if (error instanceof CrustError && error.is("VALIDATION")) {
for (const issue of error.details?.issues ?? []) {
console.error(`${issue.path}: ${issue.message}`); // => flags.port: …
}
} else if (error instanceof Error && error.name === "AbortError") {
// cancelled
} else {
throw error;
}
}.is(code) narrows details to that code's shape. .withCause(cause) sets cause and returns the error. toJSON() returns code, message, and details, and omits cause. The constructor's details argument is required for COMMAND_NOT_FOUND and ENV. See the declaration for the exact generic contract.
Codes
| Code | Meaning |
|---|---|
DEFINITION | Malformed dynamic definition or collision, recipe, Extension, Context, or documentation-section failure |
PARSE | Argv syntax, structured run() input binding, or built-in value parsing failed |
VALIDATION | Required-value or Standard Schema validation failed |
COMMAND_NOT_FOUND | An unknown command under the routing rules |
ENV | @crustjs/env variables were missing or invalid |
With execute(), an unmatched subcommand token becomes a positional argument if the parent has a Command Action; without an action, the parent reports COMMAND_NOT_FOUND. Typed run() returns COMMAND_NOT_FOUND for any unknown path segment.
Details
CrustErrorDetailsMap
Prop
Type
DefinitionErrorDetails
Prop
Type
Context failures set reason: missing-context for an unprovided Context, context-cycle for a dependency cycle, flags-before-validation when a flag-owning Context is pulled before validation, context-during-disposal when a never-built Context is pulled once cleanup has begun, and context-after-disposal for any pull after disposal.
ParseErrorDetails
Prop
Type
value is retained for compatibility; Core no longer populates it.
ValidationErrorDetails
Prop
Type
Every schema failure in one invocation is aggregated into one VALIDATION error. Issue paths are normalized under args.<name> or flags.<name>.
CommandNotFoundErrorDetails
Prop
Type
available lists visible children only; parentCommand is a serializable snapshot.
EnvErrorDetails
Prop
Type
EnvIssue
Prop
Type
One ENV error aggregates every missing or invalid variable of a defineEnv() Context. Its message, details, and toJSON() name variables and their declared expectations but never their values, and it carries no cause.
Propagation boundaries
| Failure | run() | execute() |
|---|---|---|
Authoring and attachment (.flags(), .provide(), .add(), …) | Throws at the builder call | Throws at the builder call |
| Preparation | failed outcome; no hooks run | Rendered directly; exit 1 |
| Routing, parsing | failed outcome; no hooks run | Rendered via onError; exit 1 |
preRun, validation, Context setup, Command Action | failed outcome after postRun hooks | Rendered via onError; exit 1 |
Cancellation (AbortError) | failed outcome | Offered to onError, else silent; exit 130 |
snapshot() rejects with preparation errors. Context setup and Command Action failures are not wrapped in CrustError.
- After a lifecycle failure, every
postRunhook still runs in reverse registration order, and constructed Contexts are disposed afterwards. - An invocation failure takes precedence over
postRunfailures. After a successful invocation, the firstpostRunfailure escapes once the remaining hooks run. - When invocation and disposal both fail, the escaping error is a
SuppressedError:.erroris the disposal failure and.suppressedis the invocation failure. run()returns the escaping error and all captured output, including cleanup output. It never callsonErroror changesprocess.exitCode.
execute() renders a failure once. Extension onError hooks are presentation only and cannot turn a failure into success: they run in registration order until one returns a truthy value. If none claims the failure, Core's renderer writes one Error: message line per underlying failure (each member of a SuppressedError chain or AggregateError). If cleanup fails after a failure was already rendered, Core writes Cleanup failed: message lines without calling onError again.
For preRun, validation, setup, and action failures, onError runs before postRun while Contexts are live. A postRun or disposal failure reaches onError after cleanup, when Context pulls reject. Routing and parse failures receive empty args and flags and no Contexts. See error handling for worked examples.
Cancellation
Any Error whose name is "AbortError", including a DOMException, is cancellation.
Every invocation carries a signal, shared by Context setups, Extension hooks, and the Command Action. It aborts when the caller aborts execute({ signal }) or run(path, input, { signal }), or on the first Ctrl-C during execute(). Cancellation is cooperative: once the work settles, postRun hooks and Context disposal still run, and work that ignores the signal delays cleanup.
When work throws the signal's reason, as signal.throwIfAborted() does, that error decides the exit path. Ctrl-C and a bare controller.abort() supply an AbortError, which exits 130 silently; a custom reason such as a TimeoutError renders and exits 1. @crustjs/effect turns interruption into an AbortError regardless of the reason.
If disposal fails after cancellation, the escaping error is a SuppressedError rather than cancellation, and execute() exits 1.