Error handling in zcli
The zcli framework provides comprehensive error handling through structured error types and detailed diagnostic information. All parsing functions use standard Zig error unions to provide both type safety and rich error context.
Overview
zcli uses structured error types where parsing functions return standard Zig error unions:
- Success: The successfully parsed data (e.g.,
ArgsTypeorOptionsResult(OptionsType)) - Error: A
ZcliErrorwith detailed diagnostic information about what went wrong
Core error types
ZcliError
The main error union used throughout zcli:
pub const ZcliError = error{
// Argument parsing errors
ArgumentMissingRequired,
ArgumentInvalidValue,
ArgumentTooMany,
// Option parsing errors
OptionUnknown,
OptionMissingValue,
OptionInvalidValue,
OptionBooleanWithValue,
OptionDuplicate,
// Command routing errors
CommandNotFound,
SubcommandNotFound,
// Build-time errors
BuildCommandDiscoveryFailed,
BuildRegistryGenerationFailed,
BuildOutOfMemory,
// System errors
SystemOutOfMemory,
SystemFileNotFound,
SystemAccessDenied,
// Special cases
HelpRequested,
VersionRequested,
// Resource limits
ResourceLimitExceeded,
};
Diagnostic information
When errors occur, zcli provides detailed diagnostic information through the ZcliDiagnostic type, which includes:
- Field context: Which argument or option caused the error
- Position information: Where in the command line the error occurred
- Type expectations: What was expected vs. what was provided
- Suggestions: Smart suggestions for typos and similar names
API functions
parseArgs
Parse positional arguments into a struct:
pub fn parseArgs(comptime ArgsType: type, args: []const []const u8, diag: ?*?ZcliDiagnostic) ZcliError!ArgsType
The final diag parameter is an optional out-parameter: pass null if you only need the error, or a *?ZcliDiagnostic to capture rich context about the failure (see Diagnostic-driven error messages below).
Example:
const Args = struct {
name: []const u8,
count: u32 = 1,
};
const parsed = parseArgs(Args, &.{"Alice", "42"}, null) catch |err| switch (err) {
error.ArgumentMissingRequired => {
std.debug.print("Missing required argument\n", .{});
return;
},
error.ArgumentInvalidValue => {
std.debug.print("Invalid argument value\n", .{});
return;
},
else => return err,
};
std.debug.print("Name: {s}, Count: {}\n", .{ parsed.name, parsed.count });
parseOptions
Parse command-line options into a struct:
pub fn parseOptions(
comptime OptionsType: type,
allocator: std.mem.Allocator,
args: []const []const u8,
diag: ?*?ZcliDiagnostic,
) ZcliError!OptionsResult(OptionsType)
Example:
const Options = struct {
verbose: bool = false,
output: ?[]const u8 = null,
files: [][]const u8 = &.{},
};
const result = parseOptions(Options, allocator, args, null) catch |err| switch (err) {
error.OptionUnknown => {
std.debug.print("Unknown option provided\n", .{});
return;
},
error.OptionInvalidValue => {
std.debug.print("Invalid value for option\n", .{});
return;
},
else => return err,
};
defer cleanupOptions(Options, result.options, allocator);
// Use result.options and result.remaining_args
Error handling patterns
Basic pattern
const parsed = parseOptions(Options, allocator, args, null) catch |err| {
std.debug.print("Parse error: {}\n", .{err});
std.process.exit(1);
};
defer cleanupOptions(Options, parsed.options, allocator);
Detailed error handling
const parsed = parseOptions(Options, allocator, args, null) catch |err| switch (err) {
error.OptionUnknown => {
std.debug.print("Unknown option. Run with --help for usage.\n", .{});
std.process.exit(2);
},
error.OptionInvalidValue => {
std.debug.print("Invalid value provided for option.\n", .{});
std.process.exit(2);
},
error.ArgumentMissingRequired => {
std.debug.print("Missing required argument. Run with --help for usage.\n", .{});
std.process.exit(2);
},
error.SystemOutOfMemory => return error.SystemOutOfMemory,
else => {
std.debug.print("Unexpected error: {}\n", .{err});
std.process.exit(1);
},
};
Diagnostic-driven error messages
The catch-and-switch patterns above only know which error occurred. To tell the user what went wrong — which option, what value, what was expected — pass a diagnostic out-parameter and format it with formatDiagnostic:
var diag: ?zcli.ZcliDiagnostic = null;
const result = parseOptions(Options, allocator, args, &diag) catch {
if (diag) |d| {
const msg = try zcli.formatDiagnostic(d, allocator);
defer allocator.free(msg);
std.debug.print("{s}\n", .{msg});
}
std.process.exit(2);
};
defer cleanupOptions(Options, result.options, allocator);
The formatted messages carry the full context, for example:
Missing required argument 'name' at position 1. Expected type: []const u8
Invalid value 'abc' for option '--count'. Expected type: u32
Boolean option '--verbose' does not accept a value (got 'yes')
Unknown option '--verbos'
Did you mean:
--verbose
OptionUnknown and CommandNotFound diagnostics include “did you mean?” suggestions computed by edit distance. This is exactly how the framework produces its own error output — commands get it for free.
Framework integration
Automatic error handling
When using the zcli framework, error handling is automatic:
// In your command's execute function
const Context = @import("command_registry").Context;
pub fn execute(args: Args, options: Options, context: *Context) !void {
// args and options are already parsed and validated
// Any parsing errors were handled by the framework
try context.stdout().print("Hello, {s}!\n", .{args.name});
}
The framework automatically:
- Parses all arguments and options
- Displays user-friendly error messages for parsing errors
- Provides appropriate exit codes
- Shows help information when requested
Command errors
Commands can return errors which will be handled by the framework:
pub fn execute(args: Args, options: Options, context: *Context) !void {
const io = context.io;
const file = std.Io.Dir.cwd().openFile(io, args.filename, .{}) catch |err| switch (err) {
error.FileNotFound => {
try context.stderr().print("Error: File '{s}' not found\n", .{args.filename});
return;
},
error.AccessDenied => {
try context.stderr().print("Error: Permission denied accessing '{s}'\n", .{args.filename});
return;
},
else => return err, // Let framework handle unexpected errors
};
defer file.close(io);
// Process file...
}
Memory management
Option cleanup
When using parseOptions directly, always clean up array options:
const result = try parseOptions(Options, allocator, args, null);
defer cleanupOptions(Options, result.options, allocator);
The framework handles this cleanup automatically for command implementations.
Error contexts
Error contexts are lightweight and don’t require explicit cleanup.
Best practices
- Use specific error handling: Match on specific error types rather than catching all errors
- Provide helpful messages: Give users actionable information about what went wrong
- Use appropriate exit codes:
0for success1for general errors2for usage errors (wrong arguments/options)
- Let framework handle parsing: Use command implementations instead of manual parsing when possible
- Clean up resources: Always use
deferfor cleanup, especially for array options
Examples
Complete command-line parser
For standalone parsing outside the framework, the unified entry point re-exported at the package root is zcli.parseCommandLine — it handles positionals and options together in a single pass:
const std = @import("std");
const zcli = @import("zcli");
const Args = struct {
input: []const u8,
};
const Options = struct {
verbose: bool = false,
output: ?[]const u8 = null,
threads: u32 = 1,
};
pub fn main(init: std.process.Init) !void {
const allocator = init.gpa;
const args = try init.minimal.args.toSlice(init.arena.allocator());
var diag: ?zcli.ZcliDiagnostic = null;
const result = zcli.parseCommandLine(Args, Options, null, allocator, init.environ_map, args[1..], &diag) catch |err| switch (err) {
error.SystemOutOfMemory => return error.SystemOutOfMemory,
else => {
if (diag) |d| {
const msg = try zcli.formatDiagnostic(d, allocator);
defer allocator.free(msg);
std.debug.print("{s}\n", .{msg});
} else {
std.debug.print("Parse error: {}\n", .{err});
}
std.process.exit(2);
},
};
defer result.deinit();
if (result.options.verbose) {
std.debug.print("Verbose mode enabled\n", .{});
}
if (result.options.output) |output| {
std.debug.print("Output: {s}\n", .{output});
}
std.debug.print("Input: {s}, threads: {}\n", .{ result.args.input, result.options.threads });
}
Summary
The zcli error handling system provides:
- Standard Zig patterns using error unions
- Rich diagnostic information for debugging
- Framework integration for automatic error handling
- Memory safety with clear cleanup patterns
- User-friendly messages with actionable information
- Type safety through compile-time validation
This approach ensures users get helpful error messages while developers work with familiar Zig error handling patterns.