# Integer Semantics, Safety Checks and Undefined Behaviour — Zig

Source: https://www.geekswithgeeks.com/en/zig/s-safety

> Understand overflow, wrapping and saturating arithmetic, and what build modes check.

## Safety you can see

In Zig, integer **overflow is illegal behaviour**: in Debug and ReleaseSafe builds it **panics** with a clear message; in ReleaseFast and ReleaseSmall it is undefined behaviour that the optimiser may exploit. When you want other semantics, say so explicitly: **wrapping operators** `+%`, `-%`, `*%` wrap around (useful for hashes and counters), **saturating operators** `+|`, `-|`, `*|` clamp at the limits, and builtins such as **`@addWithOverflow`** return the result and an overflow bit. Other safety checks in safe modes: **array and slice bounds**, **unwrapping a null optional** with `.?`, **`@intCast`** of an out-of-range value, **accessing the inactive field** of a tagged union, **`unreachable`** being reached, division by zero and misaligned pointer casts. Each can be disabled locally with `@setRuntimeSafety(false)` in hot code once proven correct. Zig does **not** prevent all memory errors (use-after-free, data races, dangling pointers), unlike Rust; it offers good defaults, debugging allocators, and explicitness. A common strategy is to ship **ReleaseSafe** for most software and use ReleaseFast only where measurements justify it.

## Build modes and safety

Debug and ReleaseSafe keep runtime checks; ReleaseFast and ReleaseSmall remove them.

![A two-by-two grid of tiles labelled by build mode, two with shield icons and two with speedometer icons.](assets/figures/zig/section-6-map.svg) — Figure 6.1 — Safety versus speed by build mode.

## Overflow, wrapping and saturating arithmetic

Explicit choices instead of silent wrap-around.

```zig
const std = @import("std");

pub fn main() void {
    var hits: u8 = 250;
    hits +%= 10;                           // wrapping: 260 mod 256 = 4
    std.debug.print("wrapped: {d}\n", .{hits});

    var stock: u8 = 5;
    stock -|= 8;                           // saturating: clamps at 0 instead of underflowing
    std.debug.print("saturated: {d}\n", .{stock});

    const big: u32 = 4_000_000_000;
    const result = @addWithOverflow(big, big);   // returns .{ wrapped_result, overflow_bit }
    if (result[1] == 1) {
        std.debug.print("overflow detected, wrapped value {d}\n", .{result[0]});
    }

    const percent: u8 = 120;
    const capped: u8 = @min(percent, 100);
    std.debug.print("capped: {d}\n", .{capped});

    // var x: u8 = 255;
    // x += 1;           // Debug/ReleaseSafe: panic "integer overflow"; ReleaseFast: undefined behaviour
}

fn hashBytes(data: []const u8) u32 {       // FNV-1a: wrapping multiplication is intended
    var h: u32 = 2166136261;
    for (data) |b| {
        h ^= b;
        h *%= 16777619;
    }
    return h;
}
```

## Ship ReleaseSafe by default

The cost of bounds and overflow checks is usually small. Keeping them in production turns silent memory corruption into a clear crash with a stack trace, which is much easier to diagnose.

**Quiz:** What does `x +|= 10` do for an unsigned integer near its maximum?

- [ ] Wraps around to a small number
- [ ] Panics
- [x] Saturates at the maximum value of the type
- [ ] Converts to a larger type

*Answer:* Saturates at the maximum value of the type. Saturating operators clamp to the type's range.
