Crust logoCrust

Errors

CrustError codes, structured details, and error-boundary behavior.

import { CrustError, type CrustErrorCode } from "@crustjs/core";

The class and union sketches below are readable summaries. Exact declarations retain the full generic contracts.

CrustError

Framework definition, routing, parsing, and validation failures use CrustError.

class CrustError<C extends CrustErrorCode = CrustErrorCode> extends Error {
  readonly code: C;
  readonly details: CrustErrorDetails<C>;
  cause?: unknown;

  constructor(
    code: C,
    message: string,
    ...details: undefined extends CrustErrorDetails<C>
      ? [] | [CrustErrorDetails<C>]
      : [CrustErrorDetails<C>]
  );

  is<T extends CrustErrorCode>(code: T): this is CrustError<T>;
  withCause(cause: unknown): this;
  toJSON(): { code: C; message: string; details: CrustErrorDetails<C> };
}

Use code or .is(code) instead of matching messages. The constructor's details argument is optional for codes whose entry in CrustErrorDetailsMap includes undefined; COMMAND_NOT_FOUND requires its details payload.

const outcome = await app.run(path, input);
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}`);
    }
  }
}

Codes

type CrustErrorCode = "DEFINITION" | "PARSE" | "VALIDATION" | "COMMAND_NOT_FOUND";
CodeMeaning
DEFINITIONMalformed dynamic definition or collision, recipe, Extension, Context, or documentation-section failure
PARSEArgv syntax, structured run() input binding, or built-in value parsing failed
VALIDATIONRequired-value or Standard Schema validation failed
COMMAND_NOT_FOUNDAn unknown command under the routing rules

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() resolves a failed outcome carrying COMMAND_NOT_FOUND for unknown path segments.

Details

CrustErrorDetailsMap

Prop

Type

DefinitionErrorDetails

Prop

Type

ParseErrorDetails

Prop

Type

ValidationErrorDetails

Prop

Type

Standard Schema failures are aggregated into one VALIDATION error. Paths are normalized under args.<name> or flags.<name>.

CommandNotFoundErrorDetails

Prop

Type

parentCommand is a readonly, serializable snapshot of the command whose child could not be resolved.

Propagation boundaries

Authoring and attachment errors throw at the consuming operation, and snapshot() preparation errors reject. Public run() captures preparation and invocation failures as returned outcomes.

Routing and parse failures occur before preRun, postRun, and invocation Context setup. A failure from preRun, validation, schema application, Context construction, or the Command Action becomes a failed lifecycle outcome passed to every postRun hook in reverse registration order. Constructed Contexts remain available through postRun and are disposed after the hooks complete, including after failure.

An invocation failure takes precedence over postRun failures. After a completed or finished invocation, the first postRun failure becomes the escaping error after the remaining hooks run.

const failure = new Error("database unavailable");
const app = new Crust("my-cli").action(() => {
  throw failure;
});

const outcome = await app.run([]);
// outcome is { status: "failed", error: failure, stdout: "", stderr: "" }

.run(path, input, io) returns a quiet failed outcome containing the error that escapes the complete lifecycle and all captured output, including cleanup output. It does not change process.exitCode, and Extension onError hooks never run. If invocation and disposal both fail, the escaping error is a SuppressedError whose error is the disposal failure and whose suppressed value is the invocation failure.

Context setup and Command Action failures are not wrapped in CrustError.

Pulling an unprovided Context produces a DEFINITION CrustError with details.reason === "missing-context". Its message names the Context, identifies the requesting setup when applicable, and recommends .provide(). A dynamic pull cycle reports context-cycle.

Pulling a flag-owning Context before validation reports flags-before-validation, and pulling after disposal reports context-after-disposal.

.execute() sets process.exitCode = 1, renders a failure once, and returns 1. Non-cancellation preparation failures render directly because no successfully applied Extension hook chain is available.

Extension onError hooks handle presentation only and cannot change a failed invocation into success. After preparation, execute() calls them in registration order until a hook returns a truthy value; otherwise Core renders the error to stderr.

Routing and parse failures receive a fallback hook context with empty args and flags and unavailable Contexts. For failures raised during dispatch, onError runs before postRun while Contexts are live. For postRun or disposal failures, it runs after cleanup, so Context pulls reject with context-after-disposal.

const outcome = await app.run(path, input);
if (outcome.status === "failed") {
  const { error } = outcome;
  if (error instanceof Error && error.name === "AbortError") {
    // cancellation
  }
}

Any Error whose name is "AbortError" is cancellation. run() returns cancellation in a failed outcome unless disposal changes the escaping failure. execute() sets and returns exit code 130; after successful preparation it offers the error to onError hooks, and Core renders nothing when no hook claims it.

toJSON() returns code, message, and details. It omits cause.

On this page