A blog post detailing a minimal pattern for adding error context in Zig using errdefer logging, comparing it to full diagnostics sinks and catch blocks, and discussing tradeoffs.
<header>
<h1>Minimal Viable Zig Error Contexts</h1>
<time class="meta" datetime="2026-05-03">May 3, 2026</time>
</header>
<figure class="code-block">
<pre><code><span class="line"><span class="hl-keyword">fn</span><span class="hl-function"> process_file</span>(io: Io, path: []<span class="hl-keyword">const</span> <span class="hl-type">u8</span>) <span class="hl-operator">!</span><span class="hl-type">void</span> {</span>
<span class="line"> <span class="hl-keyword">errdefer</span> log.err(<span class="hl-string">"path={s}"</span>, .{path});</span>
<span class="line"></span>
<span class="line"> <span class="hl-keyword">const</span> fd = <span class="hl-keyword">try</span> Io.Dir.cwd().openFile(io, path, .{});</span>
<span class="line"> <span class="hl-keyword">defer</span> fd.close(io);</span>
<span class="line"></span>
<span class="line"> <span class="hl-comment">// ...</span></span>
<span class="line">}</span></code></pre>
</figure>
<p>Out of the box, Zig provides minimal and sufficient facilities for error <em>handling</em> —
<a href="https://matklad.github.io/2025/11/06/error-codes-for-control-flow.html">strongly-typed error codes</a>.
Error <em>reporting</em> is left to the user. Idiomatic solution is to pass a <code>Diagnostics</code> out parameter
(“sink”) to materialize human-readable strings as needed.</p>
<p>Diagnostics pattern works well for “production” code, but for more script-y code it adds too much
friction relative to the default option of a plain
<span class="display"><code>try fallible()</code>,</span>
which of course gives a less than ideal message on failure:</p>
<figure class="code-block">
<pre><code><span class="line">λ zig build</span>
<span class="line">error: FileNotFound</span>
<span class="line">~/.cache/zig/p/../lib/std/Io/Threaded.zig:4866:35: 0x1044126c7 in dirOpenFilePosix (fail)</span>
<span class="line"> .NOENT => return error.FileNotFound,</span>
<span class="line"> ^</span>
<span class="line">~/.cache/zig/p/../lib/std/Io/Dir.zig:578:5: 0x104347d8b in openFile (fail)</span>
<span class="line"> return io.vtable.dirOpenFile(io.userdata, dir, sub_path, options);</span>
<span class="line"> ^</span>
<span class="line">~/fail/main.zig:10:16: 0x10443da5f in f (fail)</span>
<span class="line"> const fd = try Io.Dir.cwd().openFile(io, path, .{});</span>
<span class="line"> ^</span>
<span class="line">~/fail/main.zig:6:5: 0x10443db47 in main (fail)</span>
<span class="line"> try process_file(io, "data.txt");</span>
<span class="line"> ^</span></code></pre>
</figure>
<p>Error trace is helpful, but knowing <em>which</em> file is the problem is even more so.</p>
<p>The first attempt at finding a middle ground between fully-fledged diagnostics sink pattern and a
plain try is something like this:</p>
<figure class="code-block">
<pre><code><span class="line"><span class="hl-keyword">const</span> fd = dir.openFile(io, path, .{}) <span class="hl-keyword">catch</span> <span class="hl-operator">|</span>err<span class="hl-operator">|</span> {</span>
<span class="line"> log.err(<span class="hl-string">"failed to open file '{s}': {t}"</span>, .{path, err});</span>
<span class="line"> <span class="hl-keyword">return</span> err;</span>
<span class="line">}</span></code></pre>
</figure>
<p>Unsatisfactory. The friction is high, you need to come up with a reasonably-sounding error message,
the “happy path” of the code is obscured, and you need to repeat this for every fallible operation.</p>
<p>A worse-is-better version of the above code is</p>
<figure class="code-block">
<pre><code><span class="line"><span class="hl-keyword">errdefer</span> log.err(<span class="hl-string">"path={s}"</span>, .{path});</span>
<span class="line"><span class="hl-keyword">const</span> fd = <span class="hl-keyword">try</span> dir.openFile(io, path, .{});</span></code></pre>
</figure>
<p>That is, just log error context as <code>key=value</code> pairs, guarded by <code>errdefer</code>. The result is not
pretty, but passable:</p>
<figure class="code-block">
<pre><code><span class="line">λ zig build</span>
<span class="line">error: path=./data.txt</span>
<span class="line">error: FileNotFound</span>
<span class="line">~/.cache/zig/p/../lib/std/Io/Threaded.zig:4866:35: 0x1044126c7 in dirOpenFilePosix (fail)</span>
<span class="line"> .NOENT => return error.FileNotFound,</span>
<span class="line"> ^</span>
<span class="line">~/.cache/zig/p/../lib/std/Io/Dir.zig:578:5: 0x104347d8b in openFile (fail)</span>
<span class="line"> return io.vtable.dirOpenFile(io.userdata, dir, sub_path, options);</span>
<span class="line"> ^</span>
<span class="line">~/fail/main.zig:10:16: 0x10443da5f in f (fail)</span>
<span class="line"> const fd = try Io.Dir.cwd().openFile(io, path, .{});</span>
<span class="line"> ^</span>
<span class="line">~/fail/main.zig:6:5: 0x10443db47 in main (fail)</span>
<span class="line"> try process_file(io, "data.txt");</span>
<span class="line"> ^</span></code></pre>
</figure>
<p>The friction is reduced a lot:</p>
<ul>
<li>
No need to come up with any error messages beyond existing variable names.
</li>
<li>
No need to change any of the <code>try</code>s.
</li>
<li>
The context is set per-block. If a function does several fallible operations on a file, the
path needs to be specified only once.
</li>
<li>
The context is “telescopic” every function in the call-stack can add its own context.
</li>
</ul>
<p>There’s one huge drawback though — the error message is logged, even if the error is subsequently
handled. This is especially important in Zig 0.16, where cancelation
(<a href="https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p1677r2.pdf">serendipitous-success</a>)
is a possible error for any IO-ing operation, and which is intended to be handled, rather than
reported.</p>
<hr>
<p>Generalizing:</p>
<ul>
<li>
Happy path adds context to all operations in-progress.
</li>
<li>
Errors materialize current context.
</li>
</ul>
<p>This does feel like a better error management strategy than decorating errors individually, when
they happen. I wonder which language features facilitate this style?</p>
<p>This article
<a href="https://goldstein.lol/posts/error-progress/" class="display url">https://goldstein.lol/posts/error-progress/</a>
rather convincingly argues that the answer might be “none”?</p>
# Minimal Viable Zig Error Contexts
Source: [https://matklad.github.io/2026/05/03/zig-error-context.html](https://matklad.github.io/2026/05/03/zig-error-context.html)
May 3, 2026```
fn process_file(io: Io, path: []const u8) !void {
errdefer log.err("path={s}", .{path});
const fd = try Io.Dir.cwd().openFile(io, path, .{});
defer fd.close(io);
// ...
}
```
Out of the box, Zig provides minimal and sufficient facilities for error*handling*—[strongly\-typed error codes](https://matklad.github.io/2025/11/06/error-codes-for-control-flow.html)\. Error*reporting*is left to the user\. Idiomatic solution is to pass a`Diagnostics`out parameter \(“sink”\) to materialize human\-readable strings as needed\.
Diagnostics pattern works well for “production” code, but for more script\-y code it adds too much friction relative to the default option of a plain`try fallible\(\)`,which of course gives a less than ideal message on failure:
```
λ zig build
error: FileNotFound
~/.cache/zig/p/../lib/std/Io/Threaded.zig:4866:35: 0x1044126c7 in dirOpenFilePosix (fail)
.NOENT => return error.FileNotFound,
^
~/.cache/zig/p/../lib/std/Io/Dir.zig:578:5: 0x104347d8b in openFile (fail)
return io.vtable.dirOpenFile(io.userdata, dir, sub_path, options);
^
~/fail/main.zig:10:16: 0x10443da5f in f (fail)
const fd = try Io.Dir.cwd().openFile(io, path, .{});
^
~/fail/main.zig:6:5: 0x10443db47 in main (fail)
try process_file(io, "data.txt");
^
```
Error trace is helpful, but knowing*which*file is the problem is even more so\.
The first attempt at finding a middle ground between fully\-fledged diagnostics sink pattern and a plain try is something like this:
```
const fd = dir.openFile(io, path, .{}) catch |err| {
log.err("failed to open file '{s}': {t}", .{path, err});
return err;
}
```
Unsatisfactory\. The friction is high, you need to come up with a reasonably\-sounding error message, the “happy path” of the code is obscured, and you need to repeat this for every fallible operation\.
A worse\-is\-better version of the above code is
```
errdefer log.err("path={s}", .{path});
const fd = try dir.openFile(io, path, .{});
```
That is, just log error context as`key=value`pairs, guarded by`errdefer`\. The result is not pretty, but passable:
```
λ zig build
error: path=./data.txt
error: FileNotFound
~/.cache/zig/p/../lib/std/Io/Threaded.zig:4866:35: 0x1044126c7 in dirOpenFilePosix (fail)
.NOENT => return error.FileNotFound,
^
~/.cache/zig/p/../lib/std/Io/Dir.zig:578:5: 0x104347d8b in openFile (fail)
return io.vtable.dirOpenFile(io.userdata, dir, sub_path, options);
^
~/fail/main.zig:10:16: 0x10443da5f in f (fail)
const fd = try Io.Dir.cwd().openFile(io, path, .{});
^
~/fail/main.zig:6:5: 0x10443db47 in main (fail)
try process_file(io, "data.txt");
^
```
The friction is reduced a lot:
- No need to come up with any error messages beyond existing variable names\.
- No need to change any of the`try`s\.
- The context is set per\-block\. If a function does several fallible operations on a file, the path needs to be specified only once\.
- The context is “telescopic” every function in the call\-stack can add its own context\.
There’s one huge drawback though — the error message is logged, even if the error is subsequently handled\. This is especially important in Zig 0\.16, where cancelation \([serendipitous\-success](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p1677r2.pdf)\) is a possible error for any IO\-ing operation, and which is intended to be handled, rather than reported\.
---
Generalizing:
- Happy path adds context to all operations in\-progress\.
- Errors materialize current context\.
This does feel like a better error management strategy than decorating errors individually, when they happen\. I wonder which language features facilitate this style?
This article[https://goldstein\.lol/posts/error\-progress/](https://goldstein.lol/posts/error-progress/)rather convincingly argues that the answer might be “none”?
A blog post describing two tips for using `zig fmt` effectively, highlighting its 'steerable' formatting approach where trailing commas and line breaks control layout decisions, and showcasing columnar array formatting.
Zig 0.16.0 released with a new feature called 'Juicy Main' that provides dependency injection for the main() function, giving convenient access to allocators, IO, environment variables, and CLI arguments.
Zig 0.15 shows significant compile-time improvements over 0.14, with build script compilation dropping from ~7s to ~1.7s and full builds from 41s to 32s, even while still using LLVM. The article highlights progress toward self-hosted backends and incremental compilation.