Crust logoCrust

Error handling

Handle application errors, validation failures, and cancellation.

Throw an Error with a message the user can act on. Crust prints the message and exits with status 1.

import { Crust } from "@crustjs/core";

const app = new Crust("deploy").action(() => {
	throw new Error("Could not connect to api.example.com. Check your network connection.");
});

await app.execute();
$ deploy
Error: Could not connect to api.example.com. Check your network connection.
$ echo $?
1

What your user sees

$ greet
Error: Missing required flag "--name"
$ echo $?
1

Crust uses one terminal failure format whether the error comes from your code or from its checks.

FailureStderrExit code
Your ErrorError: Could not connect to api.example.com. Check your network connection.1
A Crust checkMissing flag: Error: Missing required flag "--name"
Bad choice: Error: Invalid value "Linus" for --name. Expected one of: Ada, Grace
Unknown command: Error: Unknown command "stats".
1
Ctrl+C in a promptNothing130

Let Crust validate

const manual = new Crust("greet")
	.flags({ name: "name", type: "string" })
	.action(({ flags, stdout }) => {
		if (!flags.name) throw new Error("Missing name");
		if (!["Ada", "Grace"].includes(flags.name)) throw new Error("Unknown name");
		stdout(`Hello, ${flags.name}`);
	});

export const declared = new Crust("greet")
	.flags({
		name: "name",
		type: "string",
		required: true,
		choices: ["Ada", "Grace"],
	})
	.action(({ flags, stdout }) => stdout(`Hello, ${flags.name}`));

Prefer required: true to checking for a missing value in the Command Action. The declaration keeps help text, inferred types, and terminal messages in sync. Put allowed values in choices for the same reason.

Fail from anywhere

const database = defineContext("database", () => ({
	[Symbol.dispose]() {
		console.error("Closed database");
	},
}));
function deployRelease() {
	throw new Error("Deployment service is unavailable. Try again later.");
}
export const deploy = new Crust("deploy").provide(database()).action(async ({ ctx }) => {
	await ctx.database;
	deployRelease();
});
$ deploy
Error: Deployment service is unavailable. Try again later.
Closed database
$ echo $?
1

An Error from the helper reaches the same renderer as one from a Command Action. Context disposal still runs after failure, so the Command Action does not need try/finally for Context cleanup.

Do not call process.exit(). It stops the process before Crust can run cleanup.

Do not catch an error just to print it. That hides the failure from run() and can leave the terminal exit code at 0.

Cancellation

async function createProject() {
	const name = await input({ message: "Project name?" });
	await mkdir(name);
	await writeFile(`${name}/package.json`, "{}");
}

const prompted = new Crust("scaffold").action(createProject);
$ scaffold
? Project name? ^C
$ echo $?
130

Ctrl+C in a prompt prints no error and exits with status 130. To cancel from your own code, throw new DOMException("Cancelled", "AbortError"). Put file writes and other side effects after prompts so cancellation leaves nothing to undo.

Customize the message

class ConfigError extends Error {}

const configErrors = defineExtension(defineExtensionId("config-errors"), {
	hooks: {
		onError(error, { stderr }) {
			if (!(error instanceof ConfigError)) return;
			stderr(`Error: ${error.message}`);
			stderr("Hint: Run init to create the config file.");
			return true;
		},
	},
});

const configured = new Crust("app").extend(configErrors).action(() => {
	throw new ConfigError("Config file not found.");
});

Extending the root with configErrors adds a hint for this known error class; returning true prevents the default renderer from printing it again. didYouMean() is the built-in example. See Extension hooks for the hook contract.

Error: Config file not found.
Hint: Run init to create the config file.

In tests

const outcome = await deploy.run([]);
if (outcome.status === "failed" && outcome.error instanceof Error) {
	console.log(outcome.error.message); // Deployment service is unavailable. Try again later.
}

const terminal = await captureExecute(declared, []);
console.log(terminal.stderr); // Error: Missing required flag "--name"
console.log(terminal.exitCode); // 1

run() hands your Error back unchanged in a failed outcome; captureExecute() from @crustjs/testing shows what the terminal user would see. Errors raised by Crust itself are CrustError values; narrow them with .is(code) as shown in the error reference.

On this page