<header>
<h1>Diagnostics Factory</h1>
<time class="meta" datetime="2026-02-16">Feb 16, 2026</time>
</header>
<p>In
<span class="display"><a href="https://matklad.github.io/2025/11/06/error-codes-for-control-flow.html"><em>Error Codes For Control Flow</em></a>,</span>
I explained that Zig’s strongly-typed error codes solve the “handling” half of error management,
leaving “reporting” to the users. Today, I want to describe my personal default approach to
the reporting problem, that is, showing the user a useful error message.</p>
<p>The approach is best described in the negative: <em>avoid</em> thinking about error payloads, and what
the type of error should be. Instead, provide a set of functions for constructing errors.</p>
<p>To give a concrete example, in TigerBeetle’s
<a href="https://github.com/tigerbeetle/tigerbeetle/blob/0.16.73/src/tidy.zig#L54-L188"><code>tidy.zig</code></a>
(a project-specific linting script, another useful meta-pattern), we define errors as follows:</p>
<figure class="code-block">
<pre><code><span class="line"><span class="hl-keyword">const</span> Errors = <span class="hl-keyword">struct</span> {</span>
<span class="line"> <span class="hl-keyword">pub</span> <span class="hl-keyword">fn</span><span class="hl-function"> add_long_line</span>(</span>
<span class="line"> errors: <span class="hl-operator">*</span>Errors,</span>
<span class="line"> file: SourceFile,</span>
<span class="line"> line_index: <span class="hl-type">usize</span>,</span>
<span class="line"> ) <span class="hl-type">void</span> { ... }</span>
<span class="line"></span>
<span class="line"> <span class="hl-keyword">pub</span> <span class="hl-keyword">fn</span><span class="hl-function"> add_banned</span>(</span>
<span class="line"> errors: <span class="hl-operator">*</span>Errors,</span>
<span class="line"> file: SourceFile,</span>
<span class="line"> offset: <span class="hl-type">usize</span>,</span>
<span class="line"> banned_item: []<span class="hl-keyword">const</span> <span class="hl-type">u8</span>,</span>
<span class="line"> replacement: []<span class="hl-keyword">const</span> <span class="hl-type">u8</span>,</span>
<span class="line"> ) <span class="hl-type">void</span> { ... }</span>
<span class="line"></span>
<span class="line"> <span class="hl-keyword">pub</span> <span class="hl-keyword">fn</span><span class="hl-function"> add_dead_declaration</span>(...) <span class="hl-type">void</span> { ... }</span>
<span class="line"></span>
<span class="line"> ...</span>
<span class="line">};</span></code></pre>
</figure>
<p>and the call-site looks like this:</p>
<figure class="code-block">
<pre><code><span class="line"><span class="hl-keyword">fn</span><span class="hl-function"> tidy_file</span>(file: SourceFile, errors: <span class="hl-operator">*</span>Errors) <span class="hl-type">void</span> {</span>
<span class="line"> <span class="hl-comment">// ...</span></span>
<span class="line"> <span class="hl-keyword">var</span> line_index: <span class="hl-type">usize</span> = <span class="hl-numbers">0</span>;</span>
<span class="line"> <span class="hl-keyword">while</span> (lines.next()) <span class="hl-operator">|</span>line<span class="hl-operator">|</span> : (line_index <span class="hl-operator">+=</span> <span class="hl-numbers">1</span>) {</span>
<span class="line"> <span class="hl-keyword">const</span> line_length = line_length(line);</span>
<span class="line"> <span class="hl-keyword">if</span> (line_length > <span class="hl-numbers">100</span> <span class="hl-keyword">and</span> <span class="hl-operator">!</span>contains_url(line)) {</span>
<span class="line"> errors.add_long_line(file, line_index);</span>
<span class="line"> }</span>
<span class="line"> }</span>
<span class="line">}</span></code></pre>
</figure>
<p>In this case, I collect multiple errors so I don’t return right away. Fail fast would look like
this:</p>
<figure class="code-block">
<pre><code><span class="line">errors.add_long_line(file, line_index);</span>
<span class="line"><span class="hl-keyword">return</span> <span class="hl-keyword">error</span>.Tidy;</span></code></pre>
</figure>
<p>Note that the error code is intentionally independent of the specific error produced.</p>
<hr>
<p>Some interesting properties of the solution:</p>
<ul>
<li>
The error representation is a set of constructor functions, the calling code doesn’t care what
<em>actually</em> happens inside. This is why the error factory is my <em>default</em> solution — I don’t have
to figure out up-front what I’ll do with the errors, and I can change my mind later.
</li>
<li>
There’s a natural place to convert information from the form available at the place where we emit
the error to a form useful for the user. In <code>add_banned</code> above, the caller passes in a absolute
offset in a file, and it is resolved to line number and column inside (tip: use <code>line_index</code> for
0-based internal indexes, and <code>line_number</code> for user-visible 1-based ones). Contrast this with a
traditional error as sum-type approach, where there’s a sharp syntactic discontinuity between
constructing a variant directly and calling a helper function.
</li>
<li>
This syntactic uniformity in turn allows easily grepping for all error locations:
<span class="display"><code>rg 'errors.add_'</code>.</span>
</li>
<li>
Similarly, there’s one central place that enumerates all possible errors (which is either a
benefit or a drawback).
</li>
</ul>
<p>A less trivial property is that this structure enables polymorphism. In fact, in the <code>tidy.zig</code>
code, there are two different representations of errors. When running the script, errors are
directly emitted to stderr. But when testing it, errors are collected into an in-memory buffer:</p>
<figure class="code-block">
<pre><code><span class="line"><span class="hl-keyword">pub</span> <span class="hl-keyword">fn</span><span class="hl-function"> add_banned</span>(</span>
<span class="line"> errors: <span class="hl-operator">*</span>Errors,</span>
<span class="line"> file: SourceFile,</span>
<span class="line"> offset: <span class="hl-type">usize</span>,</span>
<span class="line"> banned_item: []<span class="hl-keyword">const</span> <span class="hl-type">u8</span>,</span>
<span class="line"> replacement: []<span class="hl-keyword">const</span> <span class="hl-type">u8</span>,</span>
<span class="line">) <span class="hl-type">void</span> {</span>
<span class="line"> errors.emit(</span>
<span class="line"> <span class="hl-string">"{s}:{d}: error: {s} is banned, use {s}<span class="hl-string">\n</span>"</span>,</span>
<span class="line"> .{</span>
<span class="line"> file.path, file.line_number(offset),</span>
<span class="line"> banned_item, replacement,</span>
<span class="line"> },</span>
<span class="line"> );</span>
<span class="line">}</span>
<span class="line"></span>
<span class="line"><span class="hl-keyword">fn</span><span class="hl-function"> emit</span>(</span>
<span class="line"> errors: <span class="hl-operator">*</span>Errors,</span>
<span class="line"> <span class="hl-keyword">comptime</span> fmt: []<span class="hl-keyword">const</span> <span class="hl-type">u8</span>,</span>
<span class="line"> args: <span class="hl-type">anytype</span>,</span>
<span class="line">) <span class="hl-type">void</span> {</span>
<span class="line"> <span class="hl-keyword">comptime</span> assert(fmt[fmt.len <span class="hl-operator">-</span> <span class="hl-numbers">1</span>] <span class="hl-operator">==</span> <span class="hl-string">'<span class="hl-string">\n</span>'</span>);</span>
<span class="line"> errors.count <span class="hl-operator">+=</span> <span class="hl-numbers">1</span>;</span>
<span class="line"> <span class="hl-keyword">if</span> (errors.captured) <span class="hl-operator">|</span><span class="hl-operator">*</span>captured<span class="hl-operator">|</span> {</span>
<span class="line"> captured.writer(errors.gpa).print(fmt, args)</span>
<span class="line"> <span class="hl-keyword">catch</span> <span class="hl-built_in">@panic</span>(<span class="hl-string">"OOM"</span>);</span>
<span class="line"> } <span class="hl-keyword">else</span> {</span>
<span class="line"> std.debug.print(fmt, args);</span>
<span class="line"> }</span>
<span class="line">}</span></code></pre>
</figure>
<p>There isn’t a giant <code>union(enum)</code> of all errors, because it’s not needed for the present use-case.</p>
<p>This pattern can be further extended to a full-fledged diagnostics framework with error builders,
spans, ANSI colors and such, but that is tangential to the main idea here: even when “programming in
the small”, it might be a good idea to avoid constructing enums directly, and mandate an
intermediate function call.</p>
<hr>
<p>Two more meta observations here:</p>
<p><em>First</em>, the entire pattern is of course the expression of duality between a sum of two types and a
product of two functions (the visitor pattern)</p>
<figure class="code-block">
<pre><code><span class="line"><span class="hl-keyword">fn</span> <span class="hl-title function_">foo</span>() <span class="hl-punctuation">-></span> <span class="hl-type">Result</span><T, E>;</span>
<span class="line"></span>
<span class="line"><span class="hl-keyword">fn</span> <span class="hl-title function_">bar</span>(ok: <span class="hl-keyword">impl</span> <span class="hl-title class_">FnOnce</span>(T), err: <span class="hl-keyword">impl</span> <span class="hl-title class_">FnOnce</span>(E));</span></code></pre>
</figure>
<figure class="code-block">
<pre><code><span class="line"><span class="hl-keyword">enum</span> <span class="hl-title class_">Result</span><T, E> {</span>
<span class="line"> <span class="hl-title function_ invoke__">Ok</span>(T),</span>
<span class="line"> <span class="hl-title function_ invoke__">Err</span>(E),</span>
<span class="line">}</span>
<span class="line"></span>
<span class="line"><span class="hl-keyword">trait</span> <span class="hl-title class_">Result</span><T, E> {</span>
<span class="line"> <span class="hl-keyword">fn</span> <span class="hl-title function_">ok</span>(<span class="hl-keyword">self</span>, T);</span>
<span class="line"> <span class="hl-keyword">fn</span> <span class="hl-title function_">err</span>(<span class="hl-keyword">self</span>, E);</span>
<span class="line">}</span></code></pre>
</figure>
<p><em>Second</em>, every abstraction is a thin film separating two large bodies of code. Any interface has
two sides, the familiar one presented to the user, and the other, hidden one, presented to the
implementor. Often, default language machinery pushes you towards using the same construct for both
but that can be suboptimal. It’s natural for the user and the provider of the abstraction to
disagree on the optimal interface, and to evolve independently. Using a single big enum for errors
couples error emitting and error reporting code, as they have to meet in the middle. In contrast,
the factory solution is optimal for producer (they literally just pass whatever they already have on
hand, without any extra massaging of data), and is flexible for consumer(s).</p>
# Diagnostics Factory
Source: [https://matklad.github.io/2026/02/16/diagnostics-factory.html](https://matklad.github.io/2026/02/16/diagnostics-factory.html)
Feb 16, 2026In[*Error Codes For Control Flow*](https://matklad.github.io/2025/11/06/error-codes-for-control-flow.html),I explained that Zig’s strongly\-typed error codes solve the “handling” half of error management, leaving “reporting” to the users\. Today, I want to describe my personal default approach to the reporting problem, that is, showing the user a useful error message\.
The approach is best described in the negative:*avoid*thinking about error payloads, and what the type of error should be\. Instead, provide a set of functions for constructing errors\.
To give a concrete example, in TigerBeetle’s[`tidy\.zig`](https://github.com/tigerbeetle/tigerbeetle/blob/0.16.73/src/tidy.zig#L54-L188)\(a project\-specific linting script, another useful meta\-pattern\), we define errors as follows:
```
const Errors = struct {
pub fn add_long_line(
errors: *Errors,
file: SourceFile,
line_index: usize,
) void { ... }
pub fn add_banned(
errors: *Errors,
file: SourceFile,
offset: usize,
banned_item: []const u8,
replacement: []const u8,
) void { ... }
pub fn add_dead_declaration(...) void { ... }
...
};
```
and the call\-site looks like this:
```
fn tidy_file(file: SourceFile, errors: *Errors) void {
// ...
var line_index: usize = 0;
while (lines.next()) |line| : (line_index += 1) {
const line_length = line_length(line);
if (line_length > 100 and !contains_url(line)) {
errors.add_long_line(file, line_index);
}
}
}
```
In this case, I collect multiple errors so I don’t return right away\. Fail fast would look like this:
```
errors.add_long_line(file, line_index);
return error.Tidy;
```
Note that the error code is intentionally independent of the specific error produced\.
---
Some interesting properties of the solution:
- The error representation is a set of constructor functions, the calling code doesn’t care what*actually*happens inside\. This is why the error factory is my*default*solution — I don’t have to figure out up\-front what I’ll do with the errors, and I can change my mind later\.
- There’s a natural place to convert information from the form available at the place where we emit the error to a form useful for the user\. In`add\_banned`above, the caller passes in a absolute offset in a file, and it is resolved to line number and column inside \(tip: use`line\_index`for 0\-based internal indexes, and`line\_number`for user\-visible 1\-based ones\)\. Contrast this with a traditional error as sum\-type approach, where there’s a sharp syntactic discontinuity between constructing a variant directly and calling a helper function\.
- This syntactic uniformity in turn allows easily grepping for all error locations:`rg 'errors\.add\_'`\.
- Similarly, there’s one central place that enumerates all possible errors \(which is either a benefit or a drawback\)\.
A less trivial property is that this structure enables polymorphism\. In fact, in the`tidy\.zig`code, there are two different representations of errors\. When running the script, errors are directly emitted to stderr\. But when testing it, errors are collected into an in\-memory buffer:
```
pub fn add_banned(
errors: *Errors,
file: SourceFile,
offset: usize,
banned_item: []const u8,
replacement: []const u8,
) void {
errors.emit(
"{s}:{d}: error: {s} is banned, use {s}\n",
.{
file.path, file.line_number(offset),
banned_item, replacement,
},
);
}
fn emit(
errors: *Errors,
comptime fmt: []const u8,
args: anytype,
) void {
comptime assert(fmt[fmt.len - 1] == '\n');
errors.count += 1;
if (errors.captured) |*captured| {
captured.writer(errors.gpa).print(fmt, args)
catch @panic("OOM");
} else {
std.debug.print(fmt, args);
}
}
```
There isn’t a giant`union\(enum\)`of all errors, because it’s not needed for the present use\-case\.
This pattern can be further extended to a full\-fledged diagnostics framework with error builders, spans, ANSI colors and such, but that is tangential to the main idea here: even when “programming in the small”, it might be a good idea to avoid constructing enums directly, and mandate an intermediate function call\.
---
Two more meta observations here:
*First*, the entire pattern is of course the expression of duality between a sum of two types and a product of two functions \(the visitor pattern\)
```
fn foo() -> Result<T, E>;
fn bar(ok: impl FnOnce(T), err: impl FnOnce(E));
```
```
enum Result<T, E> {
Ok(T),
Err(E),
}
trait Result<T, E> {
fn ok(self, T);
fn err(self, E);
}
```
*Second*, every abstraction is a thin film separating two large bodies of code\. Any interface has two sides, the familiar one presented to the user, and the other, hidden one, presented to the implementor\. Often, default language machinery pushes you towards using the same construct for both but that can be suboptimal\. It’s natural for the user and the provider of the abstraction to disagree on the optimal interface, and to evolve independently\. Using a single big enum for errors couples error emitting and error reporting code, as they have to meet in the middle\. In contrast, the factory solution is optimal for producer \(they literally just pass whatever they already have on hand, without any extra massaging of data\), and is flexible for consumer\(s\)\.
Superloglabs has open-sourced Superlog, an agentic observability platform based on OpenTelemetry that automatically clusters events, generates incidents, and helps fix bugs.
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.
The article argues that disabling asserts in production is a bad practice, using Zig's assert mechanism as an example to illustrate the benefits of keeping asserts enabled for catching programming errors even in production builds.
A developer shares how visualizing failure clusters across many agent runs changed their debugging approach, emphasizing the need for a feedback loop so agents learn from past mistakes rather than treating failures as isolated bugs. The post highlights manual workarounds and a platform called BentoLabs that implements closed-loop improvement.
A practical guide for developers (especially AI coding tool users) on how to safely and efficiently use Claude Code, Codex, and other tools for multi-agent parallel development, focusing on best practices such as task decomposition, file isolation (worktree), boundary control, sequential merging, etc., to avoid file conflicts and chaos.