Safety & Build Modes
Safety Checks and Build Modes
Zig's design philosophy is "detectable illegal behavior." The compiler and runtime work together to catch bugs early, and you control how aggressively they do so through build modes. Understanding these modes is essential for writing production Zig code.
The Four Build Modes
Zig provides four optimization modes, each with different trade-offs between safety, performance, and binary size:
Debug (default)
- No optimizations applied
- Runtime safety checks enabled: integer overflow detection, out-of-bounds indexing, null pointer dereference on optionals, and unreachable code detection
- Full stack traces on panic
- Best for development and debugging
ReleaseSafe
- Optimizations enabled (like
-O2in C) - Runtime safety checks still enabled
- Best for production code where correctness matters more than peak performance
- This is what most server software should use
ReleaseFast
- Aggressive optimizations (like
-O3in C) - All runtime safety checks removed
- Best for performance-critical inner loops, games, or code that has been thoroughly tested
- Undefined behavior is possible if your code has bugs
ReleaseSmall
- Optimized for binary size
- Safety checks removed
- Best for embedded systems or WebAssembly where code size is constrained
You select the mode when building:
zig build -Doptimize=Debug
zig build -Doptimize=ReleaseSafe
zig build -Doptimize=ReleaseFast
zig build -Doptimize=ReleaseSmall
Runtime Safety Checks
In Debug and ReleaseSafe modes, Zig performs several checks at runtime:
Integer overflow: Arithmetic that overflows panics instead of silently wrapping:
var x: u8 = 255;
x += 1; // PANIC in Debug/ReleaseSafe, wraps to 0 in ReleaseFast
If you intentionally want wrapping arithmetic, use the wrapping operators:
var x: u8 = 255;
x +%= 1; // Always wraps: x is now 0
Out-of-bounds: Indexing past the end of an array or slice panics:
const arr = [_]i32{ 1, 2, 3 };
const val = arr[5]; // PANIC: index out of bounds
Unreachable: The unreachable keyword tells the compiler that a code path should never execute. In safe modes, reaching it panics. In fast modes, it is undefined behavior:
fn classify(n: u8) []const u8 {
if (n < 128) return "low";
if (n >= 128) return "high";
unreachable; // Safe modes: panic. Fast modes: UB.
}
@setRuntimeSafety
You can override the build mode's safety setting for a specific scope using @setRuntimeSafety:
fn fastUncheckedAdd(a: u32, b: u32) u32 {
@setRuntimeSafety(false);
return a + b; // No overflow check even in Debug mode
}
fn alwaysSafeAdd(a: u32, b: u32) u32 {
@setRuntimeSafety(true);
return a + b; // Overflow check even in ReleaseFast mode
}
This is useful when:
- A hot inner loop needs to skip bounds checks after you have proven correctness
- A critical function should always be checked regardless of build mode
Wrapping vs Saturating Arithmetic
Zig provides explicit operators for non-standard arithmetic:
| Operator | Meaning | Example |
|---|---|---|
+ | Checked add (panics on overflow) | 200 + 200 panics for u8 |
+% | Wrapping add | 255 +% 1 = 0 for u8 |
| `+ | ` | Saturating add |
The same variants exist for subtraction (-, -%, -|) and multiplication (*, *%, *|).
"Safety protocols engaged, Captain." In Debug mode, Zig's shields are at maximum --- every out-of-bounds access, every overflow is caught. In ReleaseFast mode, the shields are down: you gain speed but lose protection.
Your Task
Write three functions that demonstrate different arithmetic behaviors:
checkedAdd(a: u8, b: u8) ?u16--- addsaandband returns the result as au16. If the sum would exceed 255, returnnull. Otherwise, return the sum. (This simulates what a safety check does, but as a function you control.)wrappingAdd(a: u8, b: u8) u8--- addsaandbusing wrapping arithmetic (+%). Always succeeds, wraps on overflow.saturatingAdd(a: u8, b: u8) u8--- addsaandbusing saturating arithmetic (+|). Clamps to 255 instead of overflowing.