Try to make Zig error handling with payload more ergonomic, by utilizing the diagnostic pattern.
🚧 Still under development, NOT USABLE! 🚧
- Support from simple string messages to complex objects as payloads.
- Don't repeat yourself. Automatically create error sets from tagged unions.
- Compatible with Zig's error handling mechanism. Use
tryandcatch |err|as usual. We keep no-intrusion as a goal. - Allocator-covariant. If you decide not to allocate, just pass
nullwhen initializing the diagnostic. Zero thing will be allocated, e.g., messages will not be created. - Optional-covariant. If you decide you don't want any diagnostics, just pass
null. (Nearly) zero runtime overhead will be added.
Create a simple error with a message:
const Diagnostic = @import("diag_zig").Diagnostic;
const raiseMessage = @import("diag_zig").raiseMessage;
const ErrorSetFromUnion = @import("diag_zig").ErrorSetFromUnion;
// Step 1: Define your error as a tagged union
const MyError = union(enum) {
LessThanZero: i32,
LargerThanTen: i32,
};
// Step 2: Define your function with a diag parameter, type `?*Diagnostic(MyError)`
fn foo(arg: i32, diag: ?*Diagnostic(MyError)) !void {
if (arg < 0) {
// Step 3: Use `raiseMessage` to create an error with a message
try raiseMessage(diag, .{ .LessThanZero = arg }, "arg is less than zero: {}\n", .{arg});
}
}
// Then let's call the function:
// Step 4: Initialize the diagnostic
var diag = Diagnostic(MyError).init(allocator);
defer diag.deinit();
// Step 5: Call the function with the diagnostic, and enjoy your error!
foo(-1, &diag) catch |errcode| {
std.debug.assert(errcode == ErrorSetFromUnion(MyError).LessThanZero); // the error name is generated from the union above
std.debug.assert(diag.getError().?.LessThanZero == -1); // the payload is available
std.debug.assert(std.mem.eql(u8, diag.message.?, "arg is less than zero: -1\n")); // the message is also available
};And you can also propagate the diagnostic with try:
fn bar(arg: i32, diag: ?*Diagnostic(MyError)) !void {
try foo(arg, diag);
}Have complex ownership in the payload? No problem!
// A complex error with a payload that owns dynamic memory
const MyComplexError = union(enum) {
InvalidInput: struct {
input: i32,
reason: []u8,
},
// Step 1: Define your init function to create the error pointer as you need
fn init(alloc: std.mem.Allocator, comptime reason: []const u8, reasonArgs: anytype, input: i32) *MyComplexError {
const err = alloc.create(@This()) catch unreachable;
err.InvalidInput = .{
.input = input,
.reason = std.fmt.allocPrint(alloc, reason, reasonArgs) catch unreachable,
};
return err;
}
// Step 2: Define a cleanup function to free the allocated memory and destroy the error
// Will be called in `Diagnostic.deinit()`
fn cleanUp(self: **anyopaque, userData: ?*anyopaque) void {
const err: **MyComplexError = @alignCast(@ptrCast(self));
const alloc: *std.mem.Allocator = @alignCast(@ptrCast(userData.?));
alloc.free(err.*.InvalidInput.reason); // free the dynamically allocated reason
alloc.destroy(err.*); // destroy the error itself
}
};
fn complexFoo(alloc: *const std.mem.Allocator, arg: i32, diag: ?*Diagnostic(MyComplexError)) !void {
if (arg < 0) {
try raiseRef(diag, MyComplexError.init(
alloc.*,
"arg is less than zero: {}",
.{arg},
arg,
),
@constCast(alloc), // pass the allocator as user data to cleanUp()
MyComplexError.cleanUp // specify the cleanup function to run
);
}
}
// Then let's call the function as usual:
complexFoo(&alloc, -1, &diag) catch |errcode| {
std.debug.assert(errcode == ErrorSetFromUnion(MyComplexError).InvalidInput);
const err = diag.getError().?; // get the error payload
std.debug.assert(err.InvalidInput.input == -1);
std.debug.assert(std.mem.eql(u8, err.InvalidInput.reason, "arg is less than zero: -1"));
};At any time, you can turn off diagnostics by passing null:
try foo(arg, null) catch |errcode| {
// No diagnostic will be created, so no message or payload will be available,
// but you can still get the error code
std.debug.assert(errcode == ErrorSetFromUnion(MyError).LessThanZero);
};