https://github.com/deblasis/zioerrors
Error context breadcrumbs for Zig. Pin failures with structured payload, recover the chain at the boundary.
https://github.com/deblasis/zioerrors
anyhow breadcrumbs error-context error-handling zig
Last synced: 1 day ago
JSON representation
Error context breadcrumbs for Zig. Pin failures with structured payload, recover the chain at the boundary.
- Host: GitHub
- URL: https://github.com/deblasis/zioerrors
- Owner: deblasis
- License: mit
- Created: 2026-04-29T15:58:20.000Z (about 2 months ago)
- Default Branch: main
- Last Pushed: 2026-04-30T16:31:27.000Z (about 2 months ago)
- Last Synced: 2026-04-30T18:15:35.534Z (about 2 months ago)
- Topics: anyhow, breadcrumbs, error-context, error-handling, zig
- Language: Zig
- Size: 45.9 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- Contributing: CONTRIBUTING.md
- Funding: .github/FUNDING.yml
- License: LICENSE
- Agents: AGENTS.md
Awesome Lists containing this project
README
# zioerrors
Error context breadcrumbs for Zig's bare error union. Pin failures
with structured payload, recover the chain at the boundary. Inspired
by Rust's `anyhow` / `eyre` and Go's wrapped errors.
## Before / after
Without zioerrors (a typical "what failed and why" pattern):
```zig
const file = std.fs.cwd().openFile(path, .{}) catch |err| {
std.log.err(
"loading config failed: {s} (path={s}, user_id={d})",
.{ @errorName(err), path, user_id },
);
return err;
};
```
With zioerrors (4 lines, structured, chains automatically):
```zig
const file = std.fs.cwd().openFile(path, .{}) catch |err| {
return zioerrors.fail(err, @src()).ctx("loading config")
.attr("path", path).attr("user_id", user_id).err();
};
```
At the boundary:
```zig
work() catch |err| {
std.log.err("{f}", .{zioerrors.report(err)});
return err;
};
```
Output:
```
error.FileNotFound: loading config (path=/etc/app.toml, user_id=42)
at src/config.zig:42
```
If a wrapping function adds more context, the chain is preserved:
```
error.FileNotFound: starting up (phase=config)
at src/app.zig:23
caused by error.FileNotFound: loading config (path=/etc/app.toml)
at src/config.zig:42
```
## Install
```bash
zig fetch --save git+https://github.com/deblasis/zioerrors
```
Then in your `build.zig`:
```zig
const zio_dep = b.dependency("zioerrors", .{
.target = target,
.optimize = optimize,
});
exe.root_module.addImport("zioerrors", zio_dep.module("zioerrors"));
```
Requires Zig 0.16.
## Quickstart
```zig
const std = @import("std");
const zioerrors = @import("zioerrors");
pub fn main() !void {
var gpa: std.heap.DebugAllocator(.{}) = .{};
defer _ = gpa.deinit();
var ctx = zioerrors.Context.init(gpa.allocator());
defer ctx.deinit();
zioerrors.install(&ctx);
defer zioerrors.uninstall();
work() catch |err| {
var buf: [1024]u8 = undefined;
var w: std.Io.Writer = .fixed(&buf);
zioerrors.report(err).format(&w) catch {};
std.debug.print("{s}\n", .{buf[0..w.end]});
return err;
};
}
fn work() !void {
return zioerrors.failf(
error.NotImplemented,
@src(),
"demo failure (n={d})",
.{42},
);
}
```
## API
- `zioerrors.Context.init(allocator)`: create a per-thread context.
- `zioerrors.install(&ctx)` / `uninstall()`: bind / unbind for this thread.
- `zioerrors.fail(err, @src())`: push a frame, returns a Builder.
- `.ctx(msg)` / `.ctxf(fmt, args)`: set the frame message.
- `.attr(key, value)`: typed attribute (string, signed int, unsigned int, float, bool).
- `.err()`: terminal, returns the original error.
- `zioerrors.failf(err, @src(), fmt, args)`: one-shot wrap with a formatted line.
- `zioerrors.report(err)`: snapshot the chain for printing as `{f}`.
- `zioerrors.clear()`: drop frames, reset arena (call between independent operations).
## Examples
See `examples/cli/main.zig`. Run with `zig build run-example`.
## FAQ
**Why thread-local?** It keeps function signatures bare-`!T`: no
extra parameters, no `Result(T)` wrapping. The cost is one
`Context.init` / `Context.deinit` per thread plus calling `install`
once. Cross-thread error propagation is out of scope (use your own
channel and re-attach context on the receiving thread).
**Why must I pass `@src()`?** Zig has no parameter defaults and
`inline fn` does not capture the caller's `@src()`. Passing it
explicitly costs one token per call but means the recorded frame
points at your code, not at the library wrapper.
**What if I forget to `clear()`?** Stale frames from a previous
failure will appear in a later report. The fix is to `clear()` at
each boundary.
**Allocation behavior?** Zero allocation on the happy path.
Allocations happen only inside `fail`, `ctx`, `ctxf`, and `attr`
paths, and go to the per-thread arena, which is reclaimed in O(1) by
`clear()`.
**What about OOM during context capture?** If the arena fails to
allocate while recording context, the frame or attribute is silently
dropped. The original error still propagates. Trade-off: the library
never widens your error set with its own `error.OutOfMemory`.
## Design
See `docs/superpowers/SEED.md` for the original brief and
`docs/superpowers/specs/2026-04-29-zioerrors-design.md` for the v0.1
design notes.
## Compatibility
- **Zig**: 0.16.0 (tracked in CI; earlier versions are not supported).
- **Platforms**: tested on Linux (x86_64), macOS (x86_64, aarch64), Windows (x86_64).
- **Breaking changes**: pinned to the Zig 0.16 stable release cycle. A major-version bump in Zig may require a major-version bump here.
## License
MIT. Copyright (c) 2026 Alessandro De Blasis.