💥 Check out this awesome post from Hacker News 📖
📂 **Category**:
📌 **What You’ll Learn**:
I do error payloads in Zig by making a union(enum)-based
Diagnostics type for each function. These types have special methods which
remove code bloat at call sites. A Diagnostics
type can be defined inline, and the errorset of a function can generated
from the Diagnostic’s enum tag.
pub fn scan(
db: *c.sqlite,
diag: *diagnostics.FromUnion(union(enum) 💬),
): diagnostics.Error(@TypeOf(diag))!void 💬
My diagnostics module as a gist
The generated type is a wrapper around an optional payload. It generates
an error set type from the union(enum) fields.
// diagnostics.zig
pub fn FromUnion(comptime _Payload: type) type 🔥
The first thing you will want to do is set a payload while you return
an error. For this, there is the withContext method.
pub fn countRows(
alloc: std.mem.Allocator,
db: *c.sqlite,
opts: Options,
diag: *diagnostics.FromUnion(union(enum) {
SqliteError: sqlite.ErrorPayload,
OutOfMemory: void,
}),
) !usize {
const st = sqlite.prepareStmt(
alloc,
db,
"SELECT COUNT(*) FROM {0s} WHERE ({1s})",
.{ opts.table_name, opts.where_expr },
) catch |err| return switch (err) {
error.SqliteError => diag.withContext(error.SqliteError, .init(db)),
error.OutOfMemory => error.OutOfMemory,
};
// ...
}
Here, sqlite.ErrorPayload.init saves 500 bytes of error message from
sqlite. That payload gets saved to diag and the error is returned.
You would expect callsites to need tons of boilerplate, but it’s actually
very common to just need to value copy a payload from one
diag to another, and this can be done in a single line of code.
pub const BuildDiagnostics = diagnostics.FromUnion(union(enum) {
SqliteError: sqlite.ErrorPayload,
OutOfMemory: void,
// ... 15 more ...
});
pub fn build(..., diag: *BuildDiagnostics) !void {
// Choose N chunks
const n_rows = try diag.call(countRows, .{ alloc, db, opts });
const n_chunks = @max(1, n_rows / opts.chunk_size);
}
The countRows func needs 4 arguments, but the tuple only has 3.
The call method inspects
the type of countRows to determine the type of its diag arg,
instantiates the diag, calls countRows, and if there is an error, copies the
error payload to the *BuildDiagnostics.
Written explicitly, this call would be around 5 lines of code.
pub fn build(..., diag: *BuildDiagnostics) !void {
// Choose N chunks
var count_rows_diag: diagnostics.OfFunction(countRows) = .{};
const n_rows = countRows(alloc, db, opts, &count_rows_diag) catch |err| return switch (err) {
error.SqliteError => diag.withContext(error.SqliteError, count_rows_diag.get(error.SqliteError)),
error.OutOfMemory => error.OutOfMemory,
}
const n_chunks = @max(1, n_rows / opts.chunk_size);
}
At the edges, the error payload is accessible for logging or other purposes.
fn logBuildError(diag: build.BuildDiagnostics, err: build.BuildDiagnostics.Error) void {
switch (err) {
error.LoadPluginError => if (diag.get(error.LoadPluginError)) |info| {
std.log.err("failed to load plugin '{s}': {s}", .{ info.name, @errorName(info.err) });
} else {
std.log.err("failed to load plugin: unknown error", .{});
},
// ... (handle many other errors) ...
}
}
ZLS can’t infer the result of the diag.call invocations, so it can be useful to put
in an explicit type annotations.
My diagnostics module as a gist
{💬|⚡|🔥} **What’s your take?**
Share your thoughts in the comments below!
#️⃣ **#Error #payloads #Zig**
🕒 **Posted on**: 1771198987
🌟 **Want more?** Click here for more info! 🌟
