Skip to content

Error Handling

Padrone provides a structured error hierarchy so your CLI can distinguish user errors from bugs, show actionable suggestions, and set appropriate exit codes.

All Padrone errors extend PadroneError, which carries metadata for user-friendly formatting:

PadroneError (base)
├── RoutingError — unknown command, unexpected arguments
├── ValidationError — schema validation failures
├── ConfigError — config file loading/validation failures
└── ActionError — errors thrown from action handlers

Use ActionError to throw structured errors from your command handlers:

import { createPadrone, ActionError } from 'padrone';
import * as z from 'zod/v4';
const program = createPadrone('deploy')
.arguments(z.object({
env: z.enum(['staging', 'production']),
force: z.boolean().optional(),
}))
.action(async (args) => {
if (args.env === 'production' && !args.force) {
throw new ActionError('Production deploys require --force', {
exitCode: 1,
suggestions: ['Use --force to deploy to production'],
});
}
await deploy(args.env);
});

Every PadroneError carries:

PropertyTypeDescription
messagestringHuman-readable error message
exitCodenumberProcess exit code (default: 1)
suggestionsstring[]Actionable hints shown to the user
commandstring | undefinedThe command path that produced the error
phasestring | undefinedWhich phase failed: 'parse', 'validate', 'execute', or 'config'
causeunknownOriginal error for chaining

cli() throws errors. Catch them to customize the exit behavior:

try {
await program.cli();
} catch (error) {
if (error instanceof PadroneError) {
console.error(error.message);
if (error.suggestions.length > 0) {
console.error('\nSuggestions:');
for (const s of error.suggestions) {
console.error(` - ${s}`);
}
}
process.exit(error.exitCode);
}
throw error; // Re-throw unexpected errors
}

eval() uses soft error handling — validation failures are returned as argsResult.issues rather than thrown:

const result = await program.eval('deploy --env invalid');
if (result.argsResult?.issues) {
for (const issue of result.argsResult.issues) {
console.error(`${issue.path?.join('.')}: ${issue.message}`);
}
}
if (result.error) {
// Action threw an error
console.error(result.error);
}

ValidationError carries the structured issues from schema validation:

import { ValidationError } from 'padrone';
try {
await program.cli();
} catch (error) {
if (error instanceof ValidationError) {
for (const issue of error.issues) {
console.error(` ${issue.path?.join('.') ?? ''}: ${issue.message}`);
}
}
}

Interceptors can intercept errors in the error phase to log, transform, or suppress them. The built-in help extension uses this same mechanism to display help text alongside routing/validation errors in CLI mode.

import { defineInterceptor } from 'padrone';
const errorReporter = defineInterceptor({ name: 'error-reporter' }, () => ({
error: (ctx, next) => {
reportToSentry(ctx.error);
return next(); // Pass through
},
}));
const errorRecovery = defineInterceptor({ name: 'error-recovery' }, () => ({
error: (ctx, next) => {
if (ctx.error instanceof NetworkError) {
// Suppress error and return fallback result
return { error: undefined, result: cachedValue };
}
// Transform the error
return { error: new ActionError('Something went wrong', { cause: ctx.error }) };
},
}));

The error phase only runs during eval() and cli(). See the Interceptors & Extensions guide for full details.

All Padrone errors have a toJSON() method for non-terminal contexts (APIs, web UIs):

try {
await program.cli();
} catch (error) {
if (error instanceof PadroneError) {
// { name, message, exitCode, suggestions, command, phase }
res.status(500).json(error.toJSON());
}
}

ValidationError.toJSON() also includes the issues array.

ClassPhaseWhen
RoutingErrorparseUnknown command, unexpected arguments
ValidationErrorvalidateSchema validation failures
ConfigErrorconfigConfig file not found, invalid format
ActionErrorexecuteThrown from user action handlers
PadroneErroranyBase class for custom errors