From zig-dev
Validates proper error handling patterns in Zig code including custom error sets, error context, and error propagation. Use when writing error-prone code, reviewing error handling, or debugging error cases.
How this skill is triggered — by the user, by Claude, or both
Slash command
/zig-dev:error-handling-validatorThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
This skill ensures Zig code follows best practices for error handling, including custom error sets, proper propagation, and meaningful error context.
This skill ensures Zig code follows best practices for error handling, including custom error sets, proper propagation, and meaningful error context.
Define custom error sets for each module:
// GOOD: Module-specific error set
pub const ParseError = error{
InvalidMagic,
UnsupportedVersion,
MalformedChunk,
UnknownChunkType,
BufferTooSmall,
};
pub fn parseData(data: []const u8) ParseError!Result {
if (data.len < 4) return error.BufferTooSmall;
if (!std.mem.eql(u8, data[0..4], "MAGIC")) return error.InvalidMagic;
// ...
}
Avoid generic errors:
// BAD: No custom error set
pub fn parseData(data: []const u8) !Result {
if (data.len < 4) return error.TooSmall; // Unclear error
// ...
}
Functions that can fail return error unions:
// GOOD: Clear error union return type
pub fn execute(self: *Server) ServerError!void {
// ...
}
// GOOD: Combined error sets
pub fn run(self: *Server) (ServerError || AllocatorError)!Result {
// ...
}
// BAD: Hiding errors with void
pub fn process(self: *Server) void {
self.execute() catch |err| {
// Silently ignoring errors!
};
}
Use try for simple propagation, catch for handling:
// GOOD: Propagate errors up
pub fn loadConfig(self: *Server, path: []const u8) !void {
const file = try std.fs.cwd().openFile(path, .{});
defer file.close();
const data = try file.readToEndAlloc(self.allocator, max_size);
defer self.allocator.free(data);
const config = try Config.parse(data);
try self.applyConfig(config);
}
// BAD: Catching without handling
pub fn loadConfig(self: *Server, path: []const u8) void {
const file = std.fs.cwd().openFile(path, .{}) catch return;
// Lost error information!
}
Each module should define its error set at the top:
// src/parser.zig
pub const ParseError = error{
InvalidMagic,
UnsupportedVersion,
MalformedSection,
UnknownSectionType,
BufferTooSmall,
InvalidNameTable,
InvalidCodeSection,
};
// src/server.zig
pub const ServerError = error{
InvalidOpcode,
StackOverflow,
StackUnderflow,
RegisterOutOfBounds,
UndefinedFunction,
InvalidArity,
SpawnFailed,
};
// src/value.zig
pub const ValueError = error{
InvalidType,
ListTooLong,
ContainerTooBig,
NameTooLong,
InvalidEncoding,
};
// GOOD: Explicit error set combination
pub fn loadAndExecute(self: *Server, path: []const u8) (ParseError || ServerError || std.fs.File.OpenError)!void {
const config = try Config.parseFile(path); // ParseError || File.OpenError
try self.execute(config); // ServerError
}
// ALSO GOOD: Use anyerror for complex combinations
pub fn complexOperation() anyerror!Result {
// When combining many error sets
}
Provide context when catching errors:
// GOOD: Add context to errors
pub fn loadConfig(self: *Server, path: []const u8) !void {
const config = Config.parseFile(path) catch |err| {
std.log.err("Failed to load config from {s}: {}", .{ path, err });
return err;
};
}
// BETTER: Use std.log for debugging
pub fn execute(self: *Server) !void {
const opcode = self.fetchOpcode() catch |err| {
std.log.err("Fetch failed at IP={d}: {}", .{ self.ip, err });
return err;
};
}
// BAD: Silent error swallowing
pub fn execute(self: *Server) void {
self.fetchOpcode() catch return; // No context!
}
Clean up partial state on errors:
// GOOD: errdefer for error-path cleanup
pub fn init(allocator: Allocator) !Server {
const stack = try allocator.alloc(Value, 1024);
errdefer allocator.free(stack);
const registers = try allocator.alloc(Value, 256);
errdefer allocator.free(registers);
const heap = try allocator.alloc(u8, 65536);
errdefer allocator.free(heap);
return Server{
.allocator = allocator,
.stack = stack,
.registers = registers,
.heap = heap,
};
}
/// Executes the next instruction.
///
/// Returns:
/// - ServerError.InvalidOpcode if opcode is unknown
/// - ServerError.StackOverflow if stack is full
/// - ServerError.RegisterOutOfBounds if register index invalid
pub fn execute(self: *Server) ServerError!void {
// Implementation
}
// BAD: Losing error information
pub fn process(data: []const u8) void {
parseData(data) catch return; // What went wrong?
}
// GOOD: Log or propagate
pub fn process(data: []const u8) !void {
try parseData(data); // Propagate up
}
// ALSO GOOD: Handle specific errors
pub fn process(data: []const u8) !void {
parseData(data) catch |err| {
std.log.err("Parse failed: {}", .{err});
return err;
};
}
// BAD: Generic errors are unclear
pub fn validate(self: *Server) !void {
if (self.ip >= self.code.len) return error.Invalid;
if (self.sp >= self.stack.len) return error.Error;
}
// GOOD: Specific error names
pub fn validate(self: *Server) ServerError!void {
if (self.ip >= self.code.len) return error.InvalidInstructionPointer;
if (self.sp >= self.stack.len) return error.StackOverflow;
}
// BAD: Panic for recoverable errors
pub fn getRegister(self: *Server, index: u8) Value {
if (index >= self.registers.len) {
@panic("Register out of bounds"); // Crashes program!
}
return self.registers[index];
}
// GOOD: Return error
pub fn getRegister(self: *Server, index: u8) ServerError!Value {
if (index >= self.registers.len) {
return error.RegisterOutOfBounds;
}
return self.registers[index];
}
When to use panic:
When to use errors:
// BAD: Memory leak on error
pub fn init(allocator: Allocator) !Server {
const stack = try allocator.alloc(Value, 1024);
const registers = try allocator.alloc(Value, 256); // If this fails, stack leaks!
return Server{ .stack = stack, .registers = registers };
}
// GOOD: errdefer prevents leak
pub fn init(allocator: Allocator) !Server {
const stack = try allocator.alloc(Value, 1024);
errdefer allocator.free(stack);
const registers = try allocator.alloc(Value, 256);
errdefer allocator.free(registers);
return Server{ .stack = stack, .registers = registers };
}
// BAD: Silently ignoring errors
pub fn run(self: *Server) void {
_ = self.execute(); // Ignoring error!
}
// GOOD: Handle or propagate
pub fn run(self: *Server) !void {
try self.execute(); // Propagate
}
// ALSO GOOD: Explicitly handle
pub fn run(self: *Server) void {
self.execute() catch |err| {
std.log.err("Execution failed: {}", .{err});
self.halt();
};
}
Error, Invalid, etc.)try to propagate errors uperrdefer for cleanup on error pathserrdefer used for cleanup?catch return, catch {})?Always test error paths:
test "parse returns error on invalid magic" {
const data = "INVALID_MAGIC";
const result = parseData(data);
try std.testing.expectError(ParseError.InvalidMagic, result);
}
test "execute returns error on stack overflow" {
var server = try Server.init(std.testing.allocator);
defer server.deinit();
// Fill stack
while (server.sp < server.stack.len) {
try server.push(Value.makeInt(0));
}
// Next push should overflow
try std.testing.expectError(ServerError.StackOverflow, server.push(Value.makeInt(1)));
}
Error handling in Zig should be:
Golden Rule: If an operation can fail, return an error. Never silently ignore failures.
npx claudepluginhub code0100fun/botfiles --plugin zig-devGuides Zig troubleshooting: compiler errors, runtime panics, memory leaks, build failures, error traces, and common pitfalls.
Diagnoses compiler errors, runtime panics, memory issues, and build failures in Zig programs.
Strategies for handling errors: exceptions, error types, recovery strategies, and error propagation.