Error payloads in Zig

Published: (February 15, 2026 at 06:08 PM EST)
3 min read

Source: Hacker News

Error payloads in Zig

Feb 13 2026 – 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 can be generated inline from the Diagnostic’s enum tag.

pub fn scan(
    db: *c.sqlite,
    diag: *diagnostics.FromUnion(union(enum) {
        SqliteError: sqlite.ErrorPayload,
        OutOfMemory: void,
        LoadPluginsError: diagnostics.OfFunction(transforms.loadPlugins).ErrorPayload(error.LoadPluginsError),
    }),
) 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 {
    return struct {
        pub const Payload = _Payload;
        pub const Error = ErrorSetFromEnum(std.meta.FieldEnum(Payload));

        payload: ?Payload = null,

        // ... methods ...
    };
}

Setting a payload while returning an error

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.

Propagating diagnostics with a single line

It’s common to just need to copy a payload from one diagnostic 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 function needs four arguments, but the tuple passed to call only has three. The call method inspects the type of countRows to determine the type of its diag argument, instantiates the diagnostic, calls countRows, and if there is an error, copies the error payload to the *BuildDiagnostics.

Written explicitly, this call would be around five 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);
}

Accessing the payload at the edges

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 add explicit type annotations.

0 views
Back to Blog

Related posts

Read more »

Using Zig Functions from Python

Overview This document shows how to write a small HTTP client in Zig, expose it to C, and then call it from Python. The focus is on: Using Zig’s :0const u8 nul...