Fix Your Asserts

mlugg3 pts0 comments

You Must Fix Your Asserts | Loris Cro's Blog

Loris Cro

Personal Website

About<br>Twitter<br>Twitch<br>YouTube<br>GitHub

You Must Fix Your Asserts

May 31, 2026<br>12<br>min read • by<br>Loris Cro

Fear is the killer of the mind ...and the codebase as well.

A user on a discussion platform wrote:<br>I think “disabling asserts in prod” is a pretty common technique, yeah?

As far as I know that is probably a correct statement, but I believe it to be an irredeemably bad practice . Let’s start with some context first, since this discussion started because of how std.debug.assert works in Zig.<br>Asserts in general<br>An assert is a line of code that introduces a new fact to the program, such as “this argument can never be null”, or “this integer can never be even”, and they kinda look like this:<br>assert(my_arg != null);<br>assert(my_num % 2 != 0);

If your type system can be used to enforce one of these constraints, then you will probably want to use the facilities in your language rather than asserts.<br>For example, in Zig normal pointers (e.g. *Foo) can never be null, while optional pointers (?*Foo) can, but they also force you to check before you can access the value (and for which Zig has dedicated idioms).<br>Asserts can be used to explicitly state pre/post conditions and invariants in your code. This is useful because, if you pick good assertions, those will be able to protect you from programming mistakes better than unit tests, especially if you fuzz your code.<br>An assert is worth a thousand unit tests (and orders of magnitude more than that if you fuzz), but that’s a story for a follow-up post.<br>Asserts in Zig<br>Asserts in Zig are based on unreachable, a language feature that marks invalid code paths.<br>const Op = enum { a, b, c };

fn execute(orig_op: Op) void {<br>var op = orig_op;

if (op == .a) {<br>op = .b; // turn .a into .b

const op_cost = switch(op) {<br>.a => unreachable, // impossible to reach<br>.b => 50,<br>.c => 100,<br>};

// finalize op

In this example the .a case is always mutated into a .b case by the if statement which means that, once we reach the switch, it’s impossible to enter the .a case.<br>Another neat property of unreachable is that it can be used as a statement, but it is also valid anywhere an expression (of any type) is expected.<br>In the example above we’re computing the “cost” of an operation, and it might be that it doesn’t even make sense for .a to have an associated cost. Thanks to unreachable we don’t even have to come up with an awkward placeholder value for a case that can never happen anyway.<br>Zig’s stdlib assert function also leverages unreachable and is implemented as follows:<br>pub fn assert(ok: bool) void {<br>if (!ok) unreachable; // assertion failure

Build modes<br>Zig has multiple build modes:<br>Debug<br>ReleaseSafe<br>ReleaseFast<br>ReleaseSmall<br>This is not a setting that is necessarily global to your program: every dependency can be built in a different mode and you can even use @setRuntimeSafety for block-level granularity within a single function.<br>When an assert is tripped “illegal behavior” occurs. Checked modes (Debug, ReleaseSafe, @setRuntimeSafety(true)) guarantee a crash of your program by panicking, while unchecked modes (ReleaseFast, ReleaseSmall, @setRuntimeSafety(false)) incur “unchecked illegal behavior”.<br>In short, unchecked illegal behavior means that the program will misbehave.<br>In this particular example what happens today is that the switch statement that assigns a value to op_cost will ‘fallthrough’ to one of the other cases because of how the machine code gets generated. But that’s not guaranteed, and a different version of the compiler might generate machine code that causes a different misbehavior.<br>Here’s a godbolt link so you can see for yourself.<br>This is a sharp tool, but it’s what powers a lot of powerful optimizations, for example in our case the machine code necessary to implement the first branch of the switch statement was essentially elided from the final executable.<br>Here’s another godbolt link where you can see how an assert interacts with the subsequent switch statement in both ReleaseSafe and ReleaseFast (note how in ReleaseFast the function skips all comparisons and just returns true).<br>This is the kind of stuff that videogames and other real-time media applications rely on massively.<br>Not every assert will lead to a performance increase, but optimizing compilers have the ability to propagate unreachable information, resulting in non-local optimizations that you might not be able to easily anticipate as a programmer.<br>Zig asserts are not macros<br>When approaching Zig, one thing that surprises C/C++ developers especially, is the fact that std.debug.assert is not a macro (and FYI Zig doesn’t have macros).<br>In those languages, it is common to disable assertions in a way which essentially acts as a though every call to assert had been commented out, including whatever expression is passed to the macro.<br>This means that in C/C++ you should never put an expression with side effects into a call to assert, as that...

assert asserts code unreachable statement never

Related Articles