⚡ Zig Guide LiveUnofficial
✓ Zig 0.17.0-dev.1454+5faa79730On an older Zig?

Build Modes

const std = @import("std");
const builtin = @import("builtin");
const expect = std.testing.expect;

test "the current mode is known at compile time" {
    // These snippets are built as .small. Master lowercased the mode tags:
    // .Debug/.ReleaseSafe/.ReleaseFast/.ReleaseSmall are now
    // .debug/.safe/.fast/.small.
    try expect(builtin.mode == .small);
}

test "safety checks follow the mode" {
    // True in .debug and .safe, false in .fast and .small.
    const safety_on = switch (builtin.mode) {
        .debug, .safe => true,
        .fast, .small => false,
    };
    try expect(!safety_on); // because we are in .small
}

test "safety can be forced back on for a scope" {
    // Useful for keeping bounds checks in one risky function of an
    // otherwise ReleaseFast build.
    @setRuntimeSafety(true);
    var index: usize = 1;
    _ = &index;
    const array = [_]u8{ 1, 2, 3 };
    try expect(array[index] == 2);
}

test "branch hints and unreachable" {
    // In safety builds `unreachable` panics; in ReleaseFast it is a promise
    // to the optimiser, and reaching it is illegal behaviour.
    const value: u8 = 2;
    switch (value) {
        1, 2, 3 => {},
        else => unreachable,
    }
}
ModeSafety checksOptimisedSize
Debugyesnolarge
ReleaseSafeyesyesmedium
ReleaseFastnoyesmedium
ReleaseSmallnoyessmall

Select with -O:

zig build-exe main.zig -OReleaseSafe
zig build -Doptimize=ReleaseSafe

ReleaseSafe deserves more use than it gets

The reflex from C and C++ is that release means unchecked. Zig makes that a separate axis: ReleaseSafe is optimised and keeps bounds checks, overflow checks, and null-unwrap checks. For most software the cost is small and the alternative is silent memory corruption.

Reach for ReleaseFast when you have measured that the checks matter, not by default.

Where the checks are removed, the rules do not change

In ReleaseFast and ReleaseSmall, an out-of-bounds index is illegal behaviour, the same category as C’s undefined behaviour. The check was a diagnostic for breaking the rule, not the definition of it. Code that only works because Debug caught the panic is already wrong.

Per-scope override

@setRuntimeSafety(true);

Keeps checks in one function of an otherwise unchecked build, useful for the one routine parsing untrusted input.

Knowing the mode at compile time

@import("builtin").mode is a comptime value, so mode-specific code costs nothing at runtime: the dead branch is not compiled in.