{
  "@context": "https://schema.org",
  "@id": "https://aggr.aymericbeaumet.com/items/mitchellh-com/2026-01-21-don-t-trip-wire-yourself-testing-error-recovery-in-zig/#webpage",
  "@type": [
    "WebPage",
    "ArchiveComponent"
  ],
  "articleBody": "I've written a library called [Tripwire](https://github.com/ghostty-org/ghostty/blob/main/src/tripwire.zig)[1](https://mitchellh.com/writing/tripwire#user-content-fn-1) for injecting failures into Zig programs for the express purpose of testing error handling paths. Outside of unit tests, it is **completely optimized away** and has zero runtime cost (space or time).\n\nZig has a language feature [`errdefer`](https://ziglang.org/documentation/0.15.2/#errdefer) for running code on block exit only when an error is being returned. The idea is simple: if an error occurs, you need to \"undo\" your partial effects so that when the function returns an error, the world state can be in some well-defined state.\n\nIronically, error cleanup is one of the most error-prone parts of Zig programs and is a consistent source of resource leaks and memory corruption.\n\nThis is easily understood: error codepaths are typically much less frequently executed and triggering them in tests can be difficult. As a result, they usually are only cognitively reviewed once or twice during development, and never truly exercised until a user hits them in production.\n\n* * *\n\n## Errors in Zig\n\nThis section covers some background on how errors work in general in Zig. If you're familiar with Zig error handling, feel free to skip this section.\n\nAll functions in Zig can return [error values](https://ziglang.org/documentation/0.15.2/#Errors). An error value acts a bit like an enum and looks like this: `error.OutOfMemory`. Error values are collected into name sets known as [error sets](https://ziglang.org/documentation/0.15.2/#Error-Set-Type). And functions can return either an error or a success value using something called an [error union](https://ziglang.org/documentation/0.15.2/#Error-Union-Type).\n\nYou can wrap an error union (usually a function call result) with [`try`](https://ziglang.org/documentation/0.15.2/#try) to unwrap the value or return the error.\n\nAnd, finally, the key point for this post, you can put [`errdefer`](https://ziglang.org/documentation/0.15.2/#errdefer) anywhere and when an error is returned (either directly or via `try`), then all prior `errdefer` statements in the current scope will be executed in reverse order.\n\nAll-in-all, it looks a bit like this in a toy example:\n\n```\nfn create(alloc: Allocator) !*Widget {\n    const self = try alloc.create(Widget);\n    errdefer alloc.destroy(self);\n\n    self.* = try .init(alloc);\n    errdefer self.deinit();\n\n    const file = try std.fs.cwd().openFile(\"config.txt\", .{});\n    // etc...\n\n    return self;\n}\n```\n\nIn this example, you can see the basic path:\n\n1. After allocating room for `Widget`, if we see an error we should deallocate.\n2. After initializing the `Widget`, if we see an error we should deinitialize it.\n3. etc.\n\nThis is a quintessential Zig pattern, and you will see it all over the Zig standard library and in most Zig programs. This is an easy case where `errdefer` is *obviously correct* even without tests.\n\nBut the real world gets messy fast.[2](https://mitchellh.com/writing/tripwire#user-content-fn-2)\n\n* * *\n\n## Why So Fragile?\n\nIt is also useful background to understand why it is so difficult to test error handling codepaths in Zig.\n\nZig provides some great utilities for program correctness. First, it has a number of [runtime safety checks](https://ziglang.org/documentation/0.15.2/#Illegal-Behavior) covering things from index of bounds to null dereferencing and so on. Next, Zig's [parameterized allocators](https://ziglang.org/documentation/0.15.2/#Memory) make it easier to test out of memory scenarios, constrained memory environments, and so on. Finally, Zig has a builtin [test framework](https://ziglang.org/documentation/0.15.2/#Zig-Test) and encourages writing tests culturally.\n\nBut, it has no mechanism to trigger errors to test `errdefer`. For memory allocation errors, you can use specialized allocators like the `std.testing.failing_allocator`, but getting it exactly configured right in complex code that does many allocations and whose allocations may be conditional is tricky and fragile.\n\nYou always get `defer` testing for free if you unit test your code because defers run unconditionally on block exit. But `errdefer` only runs when errors occur, so if you can't trigger the error, you can't test the corresponding `errdefer`!\n\nAnd besides tooling, error handling code by its nature is more rarely executed and the state of the world when an error occurs is often more complex than the success path (since it is also may be dependent on *where* the error occurred).\n\n* * *\n\n## Tripwire\n\nI ultimately grew tired of eyeballing error handling code and hoping it was correct, or spending hours trying to write tests that create a perfect-but-fragile scenario to trigger an exact error path. So, I wrote [Tripwire](https://github.com/ghostty-org/ghostty/blob/main/src/tripwire.zig).\n\nTripwire is a small, single-file library [1](https://mitchellh.com/writing/tripwire#user-content-fn-1) that lets you put named points in your code where you can trigger errors during tests. Outside of tests, it is written in such a way that it is completely optimized away (takes no memory and emits no machine code).\n\nConceptually it works like this:\n\nHow tripwire injects failures\n\nClick to trigger:\n\nfn init(alloc: Allocator) !\\*Self {\n\ntry tw.check(.alloc\\_buffer);\n\nconst buf = try alloc.alloc(u8, 1024);\n\nerrdefer alloc.free(buf);\n\ntry tw.check(.open\\_file); ← error injected!\n\nconst file = try openFile(\"config\");\n\nerrdefer file.close();\n\nreturn self;\n\n}\n\n**Triggering .open\\_file:** Buffer was allocated, so `errdefer alloc.free(buf)` must run. If it's missing or wrong, the test fails with a memory leak!\n\nWith code, it looks like this:\n\n```\nconst tripwire = @import(\"tripwire.zig\");\n\n// Define a tripwire module with named failure points. The second\n// argument is the function itself to get its error set.\nconst init_tw = tripwire.module(enum {\n    alloc_buffer,\n    open_file,\n}, init);\n\nfn init(alloc: Allocator) !*Self {\n    // Check the tripwire before the fallible operation.\n    // In tests, this can be configured to return an error.\n    // In release builds, this compiles to nothing.\n    try init_tw.check(.alloc_buffer);\n    const buf = try alloc.alloc(u8, 1024);\n    errdefer alloc.free(buf);\n\n    try init_tw.check(.open_file);\n    const file = try std.fs.cwd().openFile(\"config.txt\", .{});\n    errdefer file.close();\n\n    // ...\n}\n\ntest \"init error on open_file\" {\n    // Configure the tripwire to fail at the open_file point.\n    try init_tw.errorAlways(.open_file, error.OutOfMemory);\n    // Call the function and expect the error.\n    try std.testing.expectError(error.OutOfMemory, init(std.testing.allocator));\n    // End the tripwire session and reset state.\n    // This also verifies the tripwire was actually hit.\n    try init_tw.end(.reset);\n}\n```\n\nThe key insight is that `std.testing.allocator` will fail the test if any memory is leaked. So by triggering an error at `.open_file`, we force the `errdefer alloc.free(buf)` to run. If that `errdefer` was missing or wrong, the test would fail with a memory leak. In more complex scenarios, you'd add additional state testing in the unit test after the error is tripped.\n\nAnother common pattern I use is to iterate through all possible failure points to get complete coverage:\n\n```\ntest \"init handles all error points\" {\n    for (std.meta.tags(init_tw.FailPoint)) |point| {\n        try init_tw.errorAlways(point, error.OutOfMemory);\n        try std.testing.expectError(error.OutOfMemory, init(std.testing.allocator));\n        try init_tw.end(.reset);\n    }\n}\n```\n\nAs I said before, in practice you probably need a bit more expectations after the tripwire end to verify that your world state is sensible. But even at a basic level, this makes it easy to ensure no runtime safety checks or leaks are detected.\n\nIn addition to `errorAlways`, you can use `errorAfter` to only trigger an error after a certain number of times that that failure point has been reached. And `end` will verify that it was triggered. This is useful to catch resource leakage or state corruption due to poor loop cleanup.\n\n* * *\n\n## Zero-Cost Outside of Tests\n\nOutside of tests, Tripwire emits no machine code and uses no memory; it is completely optimized away.\n\nTo do this, we use Zig's comptime to detect the test framework:\n\n```\n/// Whether our module is enabled or not.\npub const enabled = builtin.is_test;\n```\n\nAnd use that to conditionally make our functions no-ops:\n\n```\npub fn check(point: FailPoint) callconv(callingConvention()) Error!void {\n  if (comptime !enabled) return;\n  // actually do stuff\n}\n```\n\nWe also use comptime to set the calling convention so that the function is inlined if we're not enabled:\n\n```\n/// Our calling convention is inline if our tripwire module is\n/// NOT enabled, so that all calls to `check` are optimized away.\nfn callingConvention() std.builtin.CallingConvention {\n    return if (!enabled) .@\"inline\" else .auto;\n}\n```\n\nSome have told me this isn't necessary, but we've seen real compilations produce machine code with full function call overhead to a function with an empty body and only a `ret`. Inlining fixes this, until we figure out what is causing that.\n\nAnd finally, the Zig compiler only analyzes and emits code for declarations that are actively referenced (or volatile). Since no code references this state when Tripwire is disabled, none of our global state gets emitted into the binary, either.\n\n* * *\n\n## Bugs, Bugs\n\nI integrated Tripwire into Ghostty in only a handful of places and immediately uncovered [many bugs](https://github.com/ghostty-org/ghostty/pull/10401). In the initial PR I fixed ~6 `errdefer` bugs. They've all never been known to trigger in the real world but they are bugs nonetheless.\n\nAnd most importantly, the bugs are now fixed paired with unit tests that verified they exist! If I remove the fix, the tests fail!\n\nI plan to continue to integrate Tripwire into more parts of the Ghostty codebase and to ensure that I'm considering `errdefer` testing for any newly written code by myself, maintainers, or contributors.\n\nPlease, copy the file if you find it useful and use it in your own projects! Ghostty is MIT licensed and Tripwire is fully self-contained in a single file. Use it!\n\n## Footnotes\n\n1. It is a single file. Copy and paste it in your project if you want to use it. [↩](https://mitchellh.com/writing/tripwire#user-content-fnref-1) [↩2](https://mitchellh.com/writing/tripwire#user-content-fnref-1-2)\n\n2. I originally had a bunch of examples in this post but I think it made the post much too long and it's easier to just take a look at some of the commits in PR [8249](https://github.com/ghostty-org/ghostty/pull/8249) and PR [10401](https://github.com/ghostty-org/ghostty/pull/10401). [↩](https://mitchellh.com/writing/tripwire#user-content-fnref-2)\n",
  "authors": [],
  "captured_at": "2026-09-08T10:44:40.429509+00:00",
  "category": "programming",
  "content_html": "<p>I've written a library called <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/ghostty-org/ghostty/blob/main/src/tripwire.zig\">Tripwire</a><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://mitchellh.com/writing/tripwire#user-content-fn-1\">1</a> for injecting failures into Zig programs for the express purpose of testing error handling paths. Outside of unit tests, it is <strong>completely optimized away</strong> and has zero runtime cost (space or time).</p>\n<p>Zig has a language feature <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://ziglang.org/documentation/0.15.2/#errdefer\"><code>errdefer</code></a> for running code on block exit only when an error is being returned. The idea is simple: if an error occurs, you need to &quot;undo&quot; your partial effects so that when the function returns an error, the world state can be in some well-defined state.</p>\n<p>Ironically, error cleanup is one of the most error-prone parts of Zig programs and is a consistent source of resource leaks and memory corruption.</p>\n<p>This is easily understood: error codepaths are typically much less frequently executed and triggering them in tests can be difficult. As a result, they usually are only cognitively reviewed once or twice during development, and never truly exercised until a user hits them in production.</p>\n<hr />\n<h2>Errors in Zig</h2>\n<p>This section covers some background on how errors work in general in Zig. If you're familiar with Zig error handling, feel free to skip this section.</p>\n<p>All functions in Zig can return <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://ziglang.org/documentation/0.15.2/#Errors\">error values</a>. An error value acts a bit like an enum and looks like this: <code>error.OutOfMemory</code>. Error values are collected into name sets known as <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://ziglang.org/documentation/0.15.2/#Error-Set-Type\">error sets</a>. And functions can return either an error or a success value using something called an <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://ziglang.org/documentation/0.15.2/#Error-Union-Type\">error union</a>.</p>\n<p>You can wrap an error union (usually a function call result) with <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://ziglang.org/documentation/0.15.2/#try\"><code>try</code></a> to unwrap the value or return the error.</p>\n<p>And, finally, the key point for this post, you can put <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://ziglang.org/documentation/0.15.2/#errdefer\"><code>errdefer</code></a> anywhere and when an error is returned (either directly or via <code>try</code>), then all prior <code>errdefer</code> statements in the current scope will be executed in reverse order.</p>\n<p>All-in-all, it looks a bit like this in a toy example:</p>\n<pre><code><span class=\"code-snippet\" data-language=\"JavaScript\"><span class=\"syntax-source syntax-js\"><span class=\"syntax-variable syntax-other syntax-readwrite syntax-js\">fn</span> <span class=\"syntax-meta syntax-function-call syntax-js\"><span class=\"syntax-variable syntax-function syntax-js\">create</span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">(</span><span class=\"syntax-variable syntax-other syntax-readwrite syntax-js\">alloc</span>: <span class=\"syntax-variable syntax-other syntax-readwrite syntax-js\">Allocator</span></span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">)</span></span></span> <span class=\"syntax-keyword syntax-operator syntax-logical syntax-js\">!</span><span class=\"syntax-keyword syntax-operator syntax-arithmetic syntax-js\">*</span><span class=\"syntax-variable syntax-other syntax-readwrite syntax-js\">Widget</span> <span class=\"syntax-meta syntax-block syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-block syntax-js\">{</span>\n    <span class=\"syntax-storage syntax-type syntax-js\">const</span> <span class=\"syntax-variable syntax-language syntax-self syntax-js\">self</span> <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-js\">=</span> <span class=\"syntax-variable syntax-other syntax-readwrite syntax-js\">try</span> <span class=\"syntax-variable syntax-other syntax-object syntax-js\">alloc</span><span class=\"syntax-punctuation syntax-accessor syntax-js\">.</span><span class=\"syntax-meta syntax-function-call syntax-method syntax-js\"><span class=\"syntax-variable syntax-function syntax-js\">create</span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">(</span><span class=\"syntax-variable syntax-other syntax-readwrite syntax-js\">Widget</span></span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">)</span></span></span><span class=\"syntax-punctuation syntax-terminator syntax-statement syntax-js\">;</span>\n    <span class=\"syntax-variable syntax-other syntax-readwrite syntax-js\">errdefer</span> <span class=\"syntax-variable syntax-other syntax-object syntax-js\">alloc</span><span class=\"syntax-punctuation syntax-accessor syntax-js\">.</span><span class=\"syntax-meta syntax-function-call syntax-method syntax-js\"><span class=\"syntax-variable syntax-function syntax-js\">destroy</span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">(</span><span class=\"syntax-variable syntax-language syntax-self syntax-js\">self</span></span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">)</span></span></span><span class=\"syntax-punctuation syntax-terminator syntax-statement syntax-js\">;</span>\n\n    <span class=\"syntax-variable syntax-language syntax-self syntax-js\">self</span><span class=\"syntax-punctuation syntax-accessor syntax-js\">.</span><span class=\"syntax-keyword syntax-operator syntax-arithmetic syntax-js\">*</span> <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-js\">=</span> <span class=\"syntax-variable syntax-other syntax-object syntax-js\">try</span> <span class=\"syntax-punctuation syntax-accessor syntax-js\">.</span><span class=\"syntax-meta syntax-function-call syntax-method syntax-js\"><span class=\"syntax-variable syntax-function syntax-js\">init</span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">(</span><span class=\"syntax-variable syntax-other syntax-readwrite syntax-js\">alloc</span></span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">)</span></span></span><span class=\"syntax-punctuation syntax-terminator syntax-statement syntax-js\">;</span>\n    <span class=\"syntax-variable syntax-other syntax-readwrite syntax-js\">errdefer</span> <span class=\"syntax-variable syntax-language syntax-self syntax-js\">self</span><span class=\"syntax-punctuation syntax-accessor syntax-js\">.</span><span class=\"syntax-meta syntax-function-call syntax-method syntax-js\"><span class=\"syntax-variable syntax-function syntax-js\">deinit</span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">(</span></span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">)</span></span></span><span class=\"syntax-punctuation syntax-terminator syntax-statement syntax-js\">;</span>\n\n    <span class=\"syntax-storage syntax-type syntax-js\">const</span> <span class=\"syntax-variable syntax-other syntax-readwrite syntax-js\">file</span> <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-js\">=</span> <span class=\"syntax-variable syntax-other syntax-readwrite syntax-js\">try</span> <span class=\"syntax-variable syntax-other syntax-object syntax-js\">std</span><span class=\"syntax-punctuation syntax-accessor syntax-js\">.</span><span class=\"syntax-meta syntax-property syntax-object syntax-js\">fs</span><span class=\"syntax-punctuation syntax-accessor syntax-js\">.</span><span class=\"syntax-meta syntax-function-call syntax-method syntax-js\"><span class=\"syntax-variable syntax-function syntax-js\">cwd</span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">(</span></span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">)</span></span></span><span class=\"syntax-punctuation syntax-accessor syntax-js\">.</span><span class=\"syntax-meta syntax-function-call syntax-method syntax-js\"><span class=\"syntax-variable syntax-function syntax-js\">openFile</span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">(</span><span class=\"syntax-string syntax-quoted syntax-double syntax-js\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-js\">&quot;</span>config.txt<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-js\">&quot;</span></span><span class=\"syntax-punctuation syntax-separator syntax-comma syntax-js\">,</span> <span class=\"syntax-punctuation syntax-accessor syntax-js\">.</span><span class=\"syntax-meta syntax-object-literal syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-block syntax-js\">{</span><span class=\"syntax-punctuation syntax-section syntax-block syntax-js\">}</span></span></span><span class=\"syntax-meta syntax-group syntax-js\"><span class=\"syntax-punctuation syntax-section syntax-group syntax-js\">)</span></span></span><span class=\"syntax-punctuation syntax-terminator syntax-statement syntax-js\">;</span>\n    <span class=\"syntax-comment syntax-line syntax-double-slash syntax-js\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-js\">//</span> etc...\n</span>\n    <span class=\"syntax-keyword syntax-control syntax-flow syntax-js\">return</span> <span class=\"syntax-variable syntax-language syntax-self syntax-js\">self</span><span class=\"syntax-punctuation syntax-terminator syntax-statement syntax-js\">;</span>\n<span class=\"syntax-punctuation syntax-section syntax-block syntax-js\">}</span></span>\n</span></span></code></pre>\n<p>In this example, you can see the basic path:</p>\n<ol>\n<li>After allocating room for <code>Widget</code>, if we see an error we should deallocate.</li>\n<li>After initializing the <code>Widget</code>, if we see an error we should deinitialize it.</li>\n<li>etc.</li>\n</ol>\n<p>This is a quintessential Zig pattern, and you will see it all over the Zig standard library and in most Zig programs. This is an easy case where <code>errdefer</code> is <em>obviously correct</em> even without tests.</p>\n<p>But the real world gets messy fast.<a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://mitchellh.com/writing/tripwire#user-content-fn-2\">2</a></p>\n<hr />\n<h2>Why So Fragile?</h2>\n<p>It is also useful background to understand why it is so difficult to test error handling codepaths in Zig.</p>\n<p>Zig provides some great utilities for program correctness. First, it has a number of <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://ziglang.org/documentation/0.15.2/#Illegal-Behavior\">runtime safety checks</a> covering things from index of bounds to null dereferencing and so on. Next, Zig's <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://ziglang.org/documentation/0.15.2/#Memory\">parameterized allocators</a> make it easier to test out of memory scenarios, constrained memory environments, and so on. Finally, Zig has a builtin <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://ziglang.org/documentation/0.15.2/#Zig-Test\">test framework</a> and encourages writing tests culturally.</p>\n<p>But, it has no mechanism to trigger errors to test <code>errdefer</code>. For memory allocation errors, you can use specialized allocators like the <code>std.testing.failing_allocator</code>, but getting it exactly configured right in complex code that does many allocations and whose allocations may be conditional is tricky and fragile.</p>\n<p>You always get <code>defer</code> testing for free if you unit test your code because defers run unconditionally on block exit. But <code>errdefer</code> only runs when errors occur, so if you can't trigger the error, you can't test the corresponding <code>errdefer</code>!</p>\n<p>And besides tooling, error handling code by its nature is more rarely executed and the state of the world when an error occurs is often more complex than the success path (since it is also may be dependent on <em>where</em> the error occurred).</p>\n<hr />\n<h2>Tripwire</h2>\n<p>I ultimately grew tired of eyeballing error handling code and hoping it was correct, or spending hours trying to write tests that create a perfect-but-fragile scenario to trigger an exact error path. So, I wrote <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/ghostty-org/ghostty/blob/main/src/tripwire.zig\">Tripwire</a>.</p>\n<p>Tripwire is a small, single-file library <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://mitchellh.com/writing/tripwire#user-content-fn-1\">1</a> that lets you put named points in your code where you can trigger errors during tests. Outside of tests, it is written in such a way that it is completely optimized away (takes no memory and emits no machine code).</p>\n<p>Conceptually it works like this:</p>\n<p>How tripwire injects failures</p>\n<p>Click to trigger:</p>\n<p>fn init(alloc: Allocator) !*Self {</p>\n<p>try tw.check(.alloc_buffer);</p>\n<p>const buf = try alloc.alloc(u8, 1024);</p>\n<p>errdefer alloc.free(buf);</p>\n<p>try tw.check(.open_file); ← error injected!</p>\n<p>const file = try openFile(&quot;config&quot;);</p>\n<p>errdefer file.close();</p>\n<p>return self;</p>\n<p>}</p>\n<p><strong>Triggering .open_file:</strong> Buffer was allocated, so <code>errdefer alloc.free(buf)</code> must run. If it's missing or wrong, the test fails with a memory leak!</p>\n<p>With code, it looks like this:</p>\n<pre><code><span class=\"code-snippet\">const tripwire = @import(&quot;tripwire.zig&quot;);\n\n// Define a tripwire module with named failure points. The second\n// argument is the function itself to get its error set.\nconst init_tw = tripwire.module(enum {\n    alloc_buffer,\n    open_file,\n}, init);\n\nfn init(alloc: Allocator) !*Self {\n    // Check the tripwire before the fallible operation.\n    // In tests, this can be configured to return an error.\n    // In release builds, this compiles to nothing.\n    try init_tw.check(.alloc_buffer);\n    const buf = try alloc.alloc(u8, 1024);\n    errdefer alloc.free(buf);\n\n    try init_tw.check(.open_file);\n    const file = try std.fs.cwd().openFile(&quot;config.txt&quot;, .{});\n    errdefer file.close();\n\n    // ...\n}\n\ntest &quot;init error on open_file&quot; {\n    // Configure the tripwire to fail at the open_file point.\n    try init_tw.errorAlways(.open_file, error.OutOfMemory);\n    // Call the function and expect the error.\n    try std.testing.expectError(error.OutOfMemory, init(std.testing.allocator));\n    // End the tripwire session and reset state.\n    // This also verifies the tripwire was actually hit.\n    try init_tw.end(.reset);\n}\n</span></code></pre>\n<p>The key insight is that <code>std.testing.allocator</code> will fail the test if any memory is leaked. So by triggering an error at <code>.open_file</code>, we force the <code>errdefer alloc.free(buf)</code> to run. If that <code>errdefer</code> was missing or wrong, the test would fail with a memory leak. In more complex scenarios, you'd add additional state testing in the unit test after the error is tripped.</p>\n<p>Another common pattern I use is to iterate through all possible failure points to get complete coverage:</p>\n<pre><code><span class=\"code-snippet\" data-language=\"CSS\"><span class=\"syntax-source syntax-css\"><span class=\"syntax-meta syntax-selector syntax-css\">test &quot;init handles all error points&quot; </span><span class=\"syntax-meta syntax-property-list syntax-css\"><span class=\"syntax-punctuation syntax-section syntax-property-list syntax-css\">{</span>\n    <span class=\"syntax-meta syntax-property-name syntax-css\">for</span> (<span class=\"syntax-meta syntax-property-name syntax-css\">std</span>.<span class=\"syntax-meta syntax-property-name syntax-css\">meta</span>.<span class=\"syntax-meta syntax-property-name syntax-css\">tags</span>(<span class=\"syntax-meta syntax-property-name syntax-css\">init</span>_<span class=\"syntax-meta syntax-property-name syntax-css\">tw</span>.F<span class=\"syntax-meta syntax-property-name syntax-css\">ail</span>P<span class=\"syntax-meta syntax-property-name syntax-css\">oint</span>)) |<span class=\"syntax-meta syntax-property-name syntax-css\">point</span>| {\n        <span class=\"syntax-meta syntax-property-name syntax-css\">try</span> <span class=\"syntax-meta syntax-property-name syntax-css\">init</span>_<span class=\"syntax-meta syntax-property-name syntax-css\">tw</span>.<span class=\"syntax-meta syntax-property-name syntax-css\">error</span>A<span class=\"syntax-meta syntax-property-name syntax-css\">lways</span>(<span class=\"syntax-meta syntax-property-name syntax-css\">point</span>, <span class=\"syntax-meta syntax-property-name syntax-css\">error</span>.O<span class=\"syntax-meta syntax-property-name syntax-css\">ut</span>O<span class=\"syntax-meta syntax-property-name syntax-css\">f</span>M<span class=\"syntax-meta syntax-property-name syntax-css\">emory</span>);\n        <span class=\"syntax-meta syntax-property-name syntax-css\">try</span> <span class=\"syntax-meta syntax-property-name syntax-css\">std</span>.<span class=\"syntax-meta syntax-property-name syntax-css\">testing</span>.<span class=\"syntax-meta syntax-property-name syntax-css\">expect</span>E<span class=\"syntax-meta syntax-property-name syntax-css\">rror</span>(<span class=\"syntax-meta syntax-property-name syntax-css\">error</span>.O<span class=\"syntax-meta syntax-property-name syntax-css\">ut</span>O<span class=\"syntax-meta syntax-property-name syntax-css\">f</span>M<span class=\"syntax-meta syntax-property-name syntax-css\">emory</span>, <span class=\"syntax-meta syntax-property-name syntax-css\">init</span>(<span class=\"syntax-meta syntax-property-name syntax-css\">std</span>.<span class=\"syntax-meta syntax-property-name syntax-css\">testing</span>.<span class=\"syntax-meta syntax-property-name syntax-css\">allocator</span>));\n        <span class=\"syntax-meta syntax-property-name syntax-css\">try</span> <span class=\"syntax-meta syntax-property-name syntax-css\">init</span>_<span class=\"syntax-meta syntax-property-name syntax-css\">tw</span>.<span class=\"syntax-meta syntax-property-name syntax-css\">end</span>(.<span class=\"syntax-meta syntax-property-name syntax-css\">reset</span>);\n</span>    <span class=\"syntax-punctuation syntax-section syntax-property-list syntax-css\">}</span>\n}\n</span></span></code></pre>\n<p>As I said before, in practice you probably need a bit more expectations after the tripwire end to verify that your world state is sensible. But even at a basic level, this makes it easy to ensure no runtime safety checks or leaks are detected.</p>\n<p>In addition to <code>errorAlways</code>, you can use <code>errorAfter</code> to only trigger an error after a certain number of times that that failure point has been reached. And <code>end</code> will verify that it was triggered. This is useful to catch resource leakage or state corruption due to poor loop cleanup.</p>\n<hr />\n<h2>Zero-Cost Outside of Tests</h2>\n<p>Outside of tests, Tripwire emits no machine code and uses no memory; it is completely optimized away.</p>\n<p>To do this, we use Zig's comptime to detect the test framework:</p>\n<pre><code><span class=\"code-snippet\">/// Whether our module is enabled or not.\npub const enabled = builtin.is_test;\n</span></code></pre>\n<p>And use that to conditionally make our functions no-ops:</p>\n<pre><code><span class=\"code-snippet\">pub fn check(point: FailPoint) callconv(callingConvention()) Error!void {\n  if (comptime !enabled) return;\n  // actually do stuff\n}\n</span></code></pre>\n<p>We also use comptime to set the calling convention so that the function is inlined if we're not enabled:</p>\n<pre><code><span class=\"code-snippet\">/// Our calling convention is inline if our tripwire module is\n/// NOT enabled, so that all calls to `check` are optimized away.\nfn callingConvention() std.builtin.CallingConvention {\n    return if (!enabled) .@&quot;inline&quot; else .auto;\n}\n</span></code></pre>\n<p>Some have told me this isn't necessary, but we've seen real compilations produce machine code with full function call overhead to a function with an empty body and only a <code>ret</code>. Inlining fixes this, until we figure out what is causing that.</p>\n<p>And finally, the Zig compiler only analyzes and emits code for declarations that are actively referenced (or volatile). Since no code references this state when Tripwire is disabled, none of our global state gets emitted into the binary, either.</p>\n<hr />\n<h2>Bugs, Bugs</h2>\n<p>I integrated Tripwire into Ghostty in only a handful of places and immediately uncovered <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/ghostty-org/ghostty/pull/10401\">many bugs</a>. In the initial PR I fixed ~6 <code>errdefer</code> bugs. They've all never been known to trigger in the real world but they are bugs nonetheless.</p>\n<p>And most importantly, the bugs are now fixed paired with unit tests that verified they exist! If I remove the fix, the tests fail!</p>\n<p>I plan to continue to integrate Tripwire into more parts of the Ghostty codebase and to ensure that I'm considering <code>errdefer</code> testing for any newly written code by myself, maintainers, or contributors.</p>\n<p>Please, copy the file if you find it useful and use it in your own projects! Ghostty is MIT licensed and Tripwire is fully self-contained in a single file. Use it!</p>\n<h2>Footnotes</h2>\n<ol>\n<li>\n<p>It is a single file. Copy and paste it in your project if you want to use it. <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://mitchellh.com/writing/tripwire#user-content-fnref-1\">↩</a> <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://mitchellh.com/writing/tripwire#user-content-fnref-1-2\">↩2</a></p>\n</li>\n<li>\n<p>I originally had a bunch of examples in this post but I think it made the post much too long and it's easier to just take a look at some of the commits in PR <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/ghostty-org/ghostty/pull/8249\">8249</a> and PR <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/ghostty-org/ghostty/pull/10401\">10401</a>. <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://mitchellh.com/writing/tripwire#user-content-fnref-2\">↩</a></p>\n</li>\n</ol>\n",
  "content_markdown": "I've written a library called [Tripwire](https://github.com/ghostty-org/ghostty/blob/main/src/tripwire.zig)[1](https://mitchellh.com/writing/tripwire#user-content-fn-1) for injecting failures into Zig programs for the express purpose of testing error handling paths. Outside of unit tests, it is **completely optimized away** and has zero runtime cost (space or time).\n\nZig has a language feature [`errdefer`](https://ziglang.org/documentation/0.15.2/#errdefer) for running code on block exit only when an error is being returned. The idea is simple: if an error occurs, you need to \"undo\" your partial effects so that when the function returns an error, the world state can be in some well-defined state.\n\nIronically, error cleanup is one of the most error-prone parts of Zig programs and is a consistent source of resource leaks and memory corruption.\n\nThis is easily understood: error codepaths are typically much less frequently executed and triggering them in tests can be difficult. As a result, they usually are only cognitively reviewed once or twice during development, and never truly exercised until a user hits them in production.\n\n* * *\n\n## Errors in Zig\n\nThis section covers some background on how errors work in general in Zig. If you're familiar with Zig error handling, feel free to skip this section.\n\nAll functions in Zig can return [error values](https://ziglang.org/documentation/0.15.2/#Errors). An error value acts a bit like an enum and looks like this: `error.OutOfMemory`. Error values are collected into name sets known as [error sets](https://ziglang.org/documentation/0.15.2/#Error-Set-Type). And functions can return either an error or a success value using something called an [error union](https://ziglang.org/documentation/0.15.2/#Error-Union-Type).\n\nYou can wrap an error union (usually a function call result) with [`try`](https://ziglang.org/documentation/0.15.2/#try) to unwrap the value or return the error.\n\nAnd, finally, the key point for this post, you can put [`errdefer`](https://ziglang.org/documentation/0.15.2/#errdefer) anywhere and when an error is returned (either directly or via `try`), then all prior `errdefer` statements in the current scope will be executed in reverse order.\n\nAll-in-all, it looks a bit like this in a toy example:\n\n```\nfn create(alloc: Allocator) !*Widget {\n    const self = try alloc.create(Widget);\n    errdefer alloc.destroy(self);\n\n    self.* = try .init(alloc);\n    errdefer self.deinit();\n\n    const file = try std.fs.cwd().openFile(\"config.txt\", .{});\n    // etc...\n\n    return self;\n}\n```\n\nIn this example, you can see the basic path:\n\n1. After allocating room for `Widget`, if we see an error we should deallocate.\n2. After initializing the `Widget`, if we see an error we should deinitialize it.\n3. etc.\n\nThis is a quintessential Zig pattern, and you will see it all over the Zig standard library and in most Zig programs. This is an easy case where `errdefer` is *obviously correct* even without tests.\n\nBut the real world gets messy fast.[2](https://mitchellh.com/writing/tripwire#user-content-fn-2)\n\n* * *\n\n## Why So Fragile?\n\nIt is also useful background to understand why it is so difficult to test error handling codepaths in Zig.\n\nZig provides some great utilities for program correctness. First, it has a number of [runtime safety checks](https://ziglang.org/documentation/0.15.2/#Illegal-Behavior) covering things from index of bounds to null dereferencing and so on. Next, Zig's [parameterized allocators](https://ziglang.org/documentation/0.15.2/#Memory) make it easier to test out of memory scenarios, constrained memory environments, and so on. Finally, Zig has a builtin [test framework](https://ziglang.org/documentation/0.15.2/#Zig-Test) and encourages writing tests culturally.\n\nBut, it has no mechanism to trigger errors to test `errdefer`. For memory allocation errors, you can use specialized allocators like the `std.testing.failing_allocator`, but getting it exactly configured right in complex code that does many allocations and whose allocations may be conditional is tricky and fragile.\n\nYou always get `defer` testing for free if you unit test your code because defers run unconditionally on block exit. But `errdefer` only runs when errors occur, so if you can't trigger the error, you can't test the corresponding `errdefer`!\n\nAnd besides tooling, error handling code by its nature is more rarely executed and the state of the world when an error occurs is often more complex than the success path (since it is also may be dependent on *where* the error occurred).\n\n* * *\n\n## Tripwire\n\nI ultimately grew tired of eyeballing error handling code and hoping it was correct, or spending hours trying to write tests that create a perfect-but-fragile scenario to trigger an exact error path. So, I wrote [Tripwire](https://github.com/ghostty-org/ghostty/blob/main/src/tripwire.zig).\n\nTripwire is a small, single-file library [1](https://mitchellh.com/writing/tripwire#user-content-fn-1) that lets you put named points in your code where you can trigger errors during tests. Outside of tests, it is written in such a way that it is completely optimized away (takes no memory and emits no machine code).\n\nConceptually it works like this:\n\nHow tripwire injects failures\n\nClick to trigger:\n\nfn init(alloc: Allocator) !\\*Self {\n\ntry tw.check(.alloc\\_buffer);\n\nconst buf = try alloc.alloc(u8, 1024);\n\nerrdefer alloc.free(buf);\n\ntry tw.check(.open\\_file); ← error injected!\n\nconst file = try openFile(\"config\");\n\nerrdefer file.close();\n\nreturn self;\n\n}\n\n**Triggering .open\\_file:** Buffer was allocated, so `errdefer alloc.free(buf)` must run. If it's missing or wrong, the test fails with a memory leak!\n\nWith code, it looks like this:\n\n```\nconst tripwire = @import(\"tripwire.zig\");\n\n// Define a tripwire module with named failure points. The second\n// argument is the function itself to get its error set.\nconst init_tw = tripwire.module(enum {\n    alloc_buffer,\n    open_file,\n}, init);\n\nfn init(alloc: Allocator) !*Self {\n    // Check the tripwire before the fallible operation.\n    // In tests, this can be configured to return an error.\n    // In release builds, this compiles to nothing.\n    try init_tw.check(.alloc_buffer);\n    const buf = try alloc.alloc(u8, 1024);\n    errdefer alloc.free(buf);\n\n    try init_tw.check(.open_file);\n    const file = try std.fs.cwd().openFile(\"config.txt\", .{});\n    errdefer file.close();\n\n    // ...\n}\n\ntest \"init error on open_file\" {\n    // Configure the tripwire to fail at the open_file point.\n    try init_tw.errorAlways(.open_file, error.OutOfMemory);\n    // Call the function and expect the error.\n    try std.testing.expectError(error.OutOfMemory, init(std.testing.allocator));\n    // End the tripwire session and reset state.\n    // This also verifies the tripwire was actually hit.\n    try init_tw.end(.reset);\n}\n```\n\nThe key insight is that `std.testing.allocator` will fail the test if any memory is leaked. So by triggering an error at `.open_file`, we force the `errdefer alloc.free(buf)` to run. If that `errdefer` was missing or wrong, the test would fail with a memory leak. In more complex scenarios, you'd add additional state testing in the unit test after the error is tripped.\n\nAnother common pattern I use is to iterate through all possible failure points to get complete coverage:\n\n```\ntest \"init handles all error points\" {\n    for (std.meta.tags(init_tw.FailPoint)) |point| {\n        try init_tw.errorAlways(point, error.OutOfMemory);\n        try std.testing.expectError(error.OutOfMemory, init(std.testing.allocator));\n        try init_tw.end(.reset);\n    }\n}\n```\n\nAs I said before, in practice you probably need a bit more expectations after the tripwire end to verify that your world state is sensible. But even at a basic level, this makes it easy to ensure no runtime safety checks or leaks are detected.\n\nIn addition to `errorAlways`, you can use `errorAfter` to only trigger an error after a certain number of times that that failure point has been reached. And `end` will verify that it was triggered. This is useful to catch resource leakage or state corruption due to poor loop cleanup.\n\n* * *\n\n## Zero-Cost Outside of Tests\n\nOutside of tests, Tripwire emits no machine code and uses no memory; it is completely optimized away.\n\nTo do this, we use Zig's comptime to detect the test framework:\n\n```\n/// Whether our module is enabled or not.\npub const enabled = builtin.is_test;\n```\n\nAnd use that to conditionally make our functions no-ops:\n\n```\npub fn check(point: FailPoint) callconv(callingConvention()) Error!void {\n  if (comptime !enabled) return;\n  // actually do stuff\n}\n```\n\nWe also use comptime to set the calling convention so that the function is inlined if we're not enabled:\n\n```\n/// Our calling convention is inline if our tripwire module is\n/// NOT enabled, so that all calls to `check` are optimized away.\nfn callingConvention() std.builtin.CallingConvention {\n    return if (!enabled) .@\"inline\" else .auto;\n}\n```\n\nSome have told me this isn't necessary, but we've seen real compilations produce machine code with full function call overhead to a function with an empty body and only a `ret`. Inlining fixes this, until we figure out what is causing that.\n\nAnd finally, the Zig compiler only analyzes and emits code for declarations that are actively referenced (or volatile). Since no code references this state when Tripwire is disabled, none of our global state gets emitted into the binary, either.\n\n* * *\n\n## Bugs, Bugs\n\nI integrated Tripwire into Ghostty in only a handful of places and immediately uncovered [many bugs](https://github.com/ghostty-org/ghostty/pull/10401). In the initial PR I fixed ~6 `errdefer` bugs. They've all never been known to trigger in the real world but they are bugs nonetheless.\n\nAnd most importantly, the bugs are now fixed paired with unit tests that verified they exist! If I remove the fix, the tests fail!\n\nI plan to continue to integrate Tripwire into more parts of the Ghostty codebase and to ensure that I'm considering `errdefer` testing for any newly written code by myself, maintainers, or contributors.\n\nPlease, copy the file if you find it useful and use it in your own projects! Ghostty is MIT licensed and Tripwire is fully self-contained in a single file. Use it!\n\n## Footnotes\n\n1. It is a single file. Copy and paste it in your project if you want to use it. [↩](https://mitchellh.com/writing/tripwire#user-content-fnref-1) [↩2](https://mitchellh.com/writing/tripwire#user-content-fnref-1-2)\n\n2. I originally had a bunch of examples in this post but I think it made the post much too long and it's easier to just take a look at some of the commits in PR [8249](https://github.com/ghostty-org/ghostty/pull/8249) and PR [10401](https://github.com/ghostty-org/ghostty/pull/10401). [↩](https://mitchellh.com/writing/tripwire#user-content-fnref-2)\n",
  "dateCreated": "2026-09-08T10:44:40.429509+00:00",
  "date_created": "2026-01-21T00:00:00+00:00",
  "date_modified": "2026-01-21T00:00:00+00:00",
  "date_published": "2026-01-21T00:00:00+00:00",
  "description": "I've written a library called Tripwire1 for injecting failures into Zig programs for the express purpose of testing error handling paths. Outside of unit tests, it is completely optimized away and has zero runtime cost (space or time). Zig…",
  "external_url": "https://mitchellh.com/writing/tripwire",
  "git": {
    "commit": "ebb0941c5c06a2ab2377342d607535262a3e969e",
    "path": "items/mitchellh-com/2026/01/2026-01-21-don-t-trip-wire-yourself-testing-error-recovery-in-zig.md",
    "permalink": "https://github.com/aymericbeaumet/aggr-instance/blob/ebb0941c5c06a2ab2377342d607535262a3e969e/items/mitchellh-com/2026/01/2026-01-21-don-t-trip-wire-yourself-testing-error-recovery-in-zig.md"
  },
  "id": "urn:aggr:item:53e1a1696428b801440f76d266e7fda3f9dc720b",
  "isBasedOn": {
    "@id": "https://mitchellh.com/writing/tripwire"
  },
  "mainEntity": {
    "@id": "https://mitchellh.com/writing/tripwire",
    "@type": "CreativeWork",
    "archivedAt": {
      "@id": "https://aggr.aymericbeaumet.com/items/mitchellh-com/2026-01-21-don-t-trip-wire-yourself-testing-error-recovery-in-zig/#webpage"
    },
    "dateModified": "2026-01-21T00:00:00+00:00",
    "datePublished": "2026-01-21T00:00:00+00:00",
    "headline": "Don't Trip[wire] Yourself: Testing Error Recovery in Zig",
    "timeRequired": "PT7M",
    "url": "https://mitchellh.com/writing/tripwire",
    "wordCount": 1507
  },
  "name": "Archived snapshot: Don't Trip[wire] Yourself: Testing Error Recovery in Zig",
  "preview": null,
  "reading_minutes": 7,
  "replicated_at": null,
  "source": "mitchellh-com",
  "source_name": "Mitchell Hashimoto",
  "summary": null,
  "tags": [],
  "temporalCoverage": "2026-09-08T10:44:40.429509+00:00",
  "title": "Don't Trip[wire] Yourself: Testing Error Recovery in Zig",
  "url": "https://aggr.aymericbeaumet.com/items/mitchellh-com/2026-01-21-don-t-trip-wire-yourself-testing-error-recovery-in-zig/",
  "word_count": 1507
}