Skip to main content

Debugging Zig Programs

Comprehensive guide to debugging Zig programs using various tools and techniques.

Debug Symbols

Strip Control

-fstrip         # Remove debug symbols
-fno-strip      # Keep debug symbols
Debug symbols are kept by default in Debug mode and removed in release modes.
zig build-exe main.zig
# Debug symbols included by default

DWARF Debug Format

-gdwarf32       # Use 32-bit DWARF format
-gdwarf64       # Use 64-bit DWARF format
DWARF is the standard debugging data format used by GDB and LLDB.

Built-in Debugging Features

Stack Traces

Zig provides automatic stack traces on panics in Debug and ReleaseSafe modes:
const std = @import("std");

fn foo() void {
    bar();
}

fn bar() void {
    baz();
}

fn baz() void {
    @panic("Something went wrong!");
}

pub fn main() void {
    foo();
}

Error Return Traces

-ferror-tracing       # Enable error return traces
-fno-error-tracing    # Disable error traces
const std = @import("std");

fn readFile() ![]const u8 {
    return error.FileNotFound;
}

fn processFile() !void {
    _ = try readFile();
}

pub fn main() !void {
    try processFile();
}

Reference Traces

-freference-trace[=N]    # Show N lines of reference trace
-fno-reference-trace     # Disable reference traces
Shows how compile errors propagate through the code:
zig build-exe -freference-trace=10 main.zig

Debug Print

const std = @import("std");

pub fn main() void {
    const x: i32 = 42;
    std.debug.print("Value of x: {d}\n", .{x});
    
    const name = "Alice";
    std.debug.print("Hello, {s}!\n", .{name});
    
    const items = [_]i32{1, 2, 3, 4, 5};
    std.debug.print("Items: {any}\n", .{items});
}

Assert

const std = @import("std");

pub fn divide(a: i32, b: i32) i32 {
    std.debug.assert(b != 0);
    return @divExact(a, b);
}

pub fn main() void {
    const result = divide(10, 2);
    std.debug.print("Result: {d}\n", .{result});
}
Assertions are only active in Debug and ReleaseSafe builds. They are compiled out in ReleaseFast and ReleaseSmall.

Using GDB

Basic GDB Workflow

zig build-exe -fno-strip main.zig

GDB Example Session

const std = @import("std");

fn add(a: i32, b: i32) i32 {
    return a + b;
}

pub fn main() void {
    const x: i32 = 10;
    const y: i32 = 20;
    const result = add(x, y);
    std.debug.print("Result: {d}\n", .{result});
}

Advanced GDB Features

(gdb) break main.zig:42 if x > 10

Using LLDB

Basic LLDB Workflow

zig build-exe -fno-strip main.zig

LLDB Example

$ zig build-exe -fno-strip debug_lldb.zig
$ lldb ./debug_lldb

(lldb) target create "./debug_lldb"
Current executable set to './debug_lldb'

(lldb) b main
Breakpoint 1: where = debug_lldb`main + ..., address = 0x...

(lldb) run
Process ... launched: './debug_lldb'
Process ... stopped
* thread #1, name = 'debug_lldb', stop reason = breakpoint 1.1
    frame #0: 0x... debug_lldb`main at debug_lldb.zig:8

(lldb) frame variable
(i32) x = 10
(i32) y = 20

(lldb) continue
Process ... resuming
Result: 30
Process ... exited with status = 0

LLDB vs GDB Commands

TaskGDBLLDB
Set breakpointbreak mainbreakpoint set --name main
Runrunprocess launch
Step overnextthread step-over
Step instepthread step-in
Continuecontinueprocess continue
Print variableprint varframe variable var
Backtracebacktracethread backtrace

Compiler Debug Options

These options are for debugging the Zig compiler itself, not your programs.

Verbose Output

--verbose-link              # Display linker invocations
--verbose-cc                # Display C compiler invocations
--verbose-air               # Show Zig AIR (Abstract IR)
--verbose-intern-pool       # Show InternPool debug output
--verbose-llvm-ir[=path]    # Show unoptimized LLVM IR
--verbose-llvm-bc=[path]    # Show unoptimized LLVM BC
--verbose-cimport           # Show C import debug output
--verbose-llvm-cpu-features # Show LLVM CPU features

Debug Compilation

--debug-compile-errors   # Crash with diagnostics on first compile error
--debug-incremental      # Enable incremental compilation debug features
--debug-rt               # Debug compiler runtime libraries
--debug-log [scope]      # Enable debug logging for scope
These flags require Zig to be built with -Denable-debug-extensions.

Memory Debugging

Valgrind Integration

-fvalgrind        # Include Valgrind client requests
-fno-valgrind     # Omit Valgrind requests
zig build-exe -O ReleaseSafe -fvalgrind memory_debug.zig

AddressSanitizer (for C code)

-fsanitize-c      # Enable UBSan for C code

ThreadSanitizer

-fsanitize-thread    # Detect data races
zig build-exe -fsanitize-thread concurrent.zig

Debug Logging

Custom Logging

const std = @import("std");
const log = std.log.scoped(.my_module);

pub fn main() void {
    log.debug("This is a debug message", .{});
    log.info("This is an info message", .{});
    log.warn("This is a warning", .{});
    log.err("This is an error", .{});
}

Log Level Control

pub const std_options = struct {
    pub const log_level = .debug;
};

Debugging Tests

zig test --test-filter "test name" test.zig
zig test --test-filter "add" math.zig

Debugging in Different Modes

Debug Mode

Best for debugging:
  • Full debug symbols
  • All safety checks enabled
  • No optimizations
  • Clear stack traces
zig build-exe -O Debug main.zig
gdb ./main

ReleaseSafe Mode

Good for finding bugs in optimized code:
  • Safety checks still enabled
  • Can add debug symbols with -fno-strip
  • Optimizations may make debugging harder
zig build-exe -O ReleaseSafe -fno-strip main.zig
gdb ./main

ReleaseFast/Small Modes

Difficult to debug:
  • Safety checks disabled
  • Heavy optimizations
  • Need -fno-strip for symbols
  • Stack traces may be incomplete
zig build-exe -O ReleaseFast -fno-strip -ferror-tracing main.zig
gdb ./main

Common Debugging Scenarios

Segmentation Fault

gdb ./program
(gdb) run
... Program received signal SIGSEGV ...
(gdb) backtrace
(gdb) frame 0
(gdb) print variable

Memory Leak

valgrind --leak-check=full --show-leak-kinds=all ./program

Infinite Loop

# In GDB/LLDB, press Ctrl+C to interrupt
(gdb) interrupt
(gdb) backtrace

Debugging Best Practices

  1. Always start with Debug mode - Use -O Debug during development
  2. Keep debug symbols - Use -fno-strip for release builds you need to debug
  3. Enable error tracing - Use -ferror-tracing in release modes
  4. Use assertions liberally - They catch bugs early in Debug/ReleaseSafe
  5. Test in ReleaseSafe - Catch optimization-exposed bugs before ReleaseFast
  6. Profile before optimizing - Don’t sacrifice debuggability unnecessarily
  7. Use logging - Better than removing debug prints
  8. Write tests - Catch bugs before debugging is needed

VSCode Integration

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Debug Zig",
            "type": "lldb",
            "request": "launch",
            "program": "${workspaceFolder}/zig-out/bin/myprogram",
            "args": [],
            "cwd": "${workspaceFolder}",
            "preLaunchTask": "zig build"
        }
    ]
}

Next Steps

Compilation Modes

Understand how different modes affect debugging

Optimization

Learn when to use which optimization level

Build docs developers (and LLMs) love