Zig Debugging

Debugging is the process of finding and fixing problems in your program. Zig gives you multiple layers of debugging support: runtime safety checks, stack traces on panics, compile-time assertions, integration with GDB and LLDB, and the ability to add structured logging. This topic covers the practical tools and techniques you use every day.

Debug vs Release Builds

  Debug build (default: zig build or zig run):
  ✓ Bounds checking on array/slice access
  ✓ Integer overflow detection
  ✓ Null pointer dereference detection
  ✓ Unreachable code detection
  ✓ Stack traces on panic
  ✓ Memory fill with 0xAA (catches use-of-uninitialized)
  ✗ Slower execution

  Release builds (zig build -Doptimize=ReleaseSafe):
  ✓ Optimized (fast)
  ✓ Safety checks still on (ReleaseSafe only)
  ✗ No debug symbols by default

Runtime Panics and Stack Traces

When a safety violation occurs in a debug build, Zig panics and prints a stack trace:

// This code panics in debug mode:
var arr = [_]u32{ 1, 2, 3 };
_ = arr[10];  // index 10 out of bounds for array of size 3
  Output:
  thread 1 panic: index out of bounds: index 10, len 3
  [path]/src/main.zig:5:12: 0x... in main
  [path]/lib/std/start.zig:...: 0x... in start

The trace shows the file, line number, and column where the panic happened. In a project, run zig build without optimization flags to keep this information.

std.debug.print — Printf-Style Debugging

const std = @import("std");

pub fn main() void {
    var x: i32 = 0;
    while (x < 5) : (x += 1) {
        std.debug.print("x={d}\n", .{x});  // always goes to stderr
    }
}

std.debug.print always writes to stderr regardless of whether your program's stdout is redirected. This prevents debug output from corrupting program output when you pipe your program's output to another command.

Compile-Time Assertions

// Crash at compile time with a clear message if condition is false
const BUFFER_SIZE = 512;
comptime {
    if (BUFFER_SIZE < 64) @compileError("BUFFER_SIZE must be at least 64");
    if (BUFFER_SIZE % 8 != 0) @compileError("BUFFER_SIZE must be a multiple of 8");
}
// Assert a type property at compile time
fn mustBePowerOfTwo(comptime n: usize) void {
    comptime {
        if (n == 0 or (n & (n - 1)) != 0) {
            @compileError("argument must be a power of two");
        }
    }
}

mustBePowerOfTwo(16);  // OK
mustBePowerOfTwo(10);  // COMPILE ERROR: argument must be a power of two

std.debug.assert — Runtime Assertions

const std = @import("std");

fn divide(a: i32, b: i32) i32 {
    std.debug.assert(b != 0);  // panics in debug if false, removed in release
    return @divTrunc(a, b);
}

pub fn main() void {
    std.debug.print("{d}\n", .{divide(10, 2)});  // 5
    std.debug.print("{d}\n", .{divide(10, 0)});  // PANIC: assertion failed
}
  assert(condition):
  Debug mode:   condition=false → panic with stack trace
  Release mode: condition removed entirely (zero cost)

Using GDB with Zig

  1. Build with debug info (default):
     zig build

  2. Launch GDB:
     gdb ./zig-out/bin/myapp

  3. Common GDB commands:
     run                    ← start the program
     break main             ← set breakpoint at main
     break src/main.zig:15  ← breakpoint at line 15
     next                   ← step over (one line)
     step                   ← step into (enter function)
     print x                ← print variable x
     info locals            ← show all local variables
     backtrace              ← show call stack
     continue               ← resume execution
     quit                   ← exit GDB

Using LLDB with Zig

  lldb ./zig-out/bin/myapp

  Common LLDB commands:
  process launch          ← start the program
  b main                  ← breakpoint at main
  n                       ← next line
  s                       ← step into
  p x                     ← print variable x
  frame variable          ← show all locals
  thread backtrace        ← show call stack
  c                       ← continue
  q                       ← quit

Valgrind — Memory Error Detection

  Build first:
  zig build

  Run with Valgrind:
  valgrind --leak-check=full ./zig-out/bin/myapp

  Valgrind detects:
  ✓ Use-after-free
  ✓ Memory leaks
  ✓ Invalid reads/writes
  ✓ Use of uninitialized values

Valgrind is available on Linux. On macOS, use Xcode Instruments. On Windows, use the Visual Studio memory profiler or Dr. Memory.

Structured Logging Pattern

const std = @import("std");

const LogLevel = enum { debug, info, warn, err };

const LEVEL = LogLevel.info;  // change to .debug for verbose output

fn log(level: LogLevel, comptime fmt: []const u8, args: anytype) void {
    if (@intFromEnum(level) < @intFromEnum(LEVEL)) return;
    const prefix = switch (level) {
        .debug => "[DEBUG]",
        .info  => "[INFO] ",
        .warn  => "[WARN] ",
        .err   => "[ERROR]",
    };
    std.debug.print("{s} " ++ fmt ++ "\n", .{prefix} ++ args);
}

pub fn main() void {
    log(.debug, "Starting up (x={d})", .{42});   // hidden at info level
    log(.info,  "Server started on port {d}", .{8080});
    log(.warn,  "Config file missing, using defaults", .{});
    log(.err,   "Database connection failed: {s}", .{"timeout"});
}

Output:

[INFO]  Server started on port 8080
[WARN]  Config file missing, using defaults
[ERROR] Database connection failed: timeout

Finding the Source of a Bug — Methodology

  Step 1: Read the panic message and stack trace
          → identifies file + line + type of violation

  Step 2: Add std.debug.print statements around the panic
          → narrow down which input caused it

  Step 3: Simplify the failing case
          → can you reproduce it in a small test?

  Step 4: Write a test that captures the bug
          → test "bug reproduction" { ... }

  Step 5: Fix the code until the test passes

  Step 6: Keep the test to prevent regression

The built-in safety checks — bounds checking, overflow detection, null dereference detection — do most of the work of finding bugs in debug builds. Enable them by using debug mode during development and only switching to release builds for performance measurement or deployment.

Leave a Comment

Your email address will not be published. Required fields are marked *