# Debug Builds

### Debug Builds

A debug build is a build made for finding mistakes.

When you are learning Zig, most of your work should happen in debug mode. It gives you stronger safety checks, better error messages, and more useful stack traces. It is slower than an optimized release build, but that is the point. While developing, correctness matters more than speed.

#### Build Modes

Zig programs can be built in different optimization modes.

The main modes are:

| Mode | Main Purpose | Safety Checks | Speed |
|---|---:|---:|---:|
| `Debug` | development and debugging | many checks enabled | slowest |
| `ReleaseSafe` | optimized but still safety-oriented | many checks enabled | faster |
| `ReleaseFast` | maximum speed | many checks disabled | fastest |
| `ReleaseSmall` | small binary size | many checks disabled | optimized for size |

For beginners, use `Debug` unless you have a specific reason not to.

When you run:

```bash
zig test main.zig
```

or:

```bash
zig build-exe main.zig
```

Zig uses debug-style behavior by default unless you ask for an optimized mode.

#### Why Debug Builds Matter

Debug builds help catch mistakes early.

For example, this program reads past the end of an array:

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

pub fn main() void {
    const numbers = [_]u8{ 10, 20, 30 };

    const x = numbers[5];

    std.debug.print("{}\n", .{x});
}
```

The array has only three items. Valid indexes are `0`, `1`, and `2`.

In a debug build, Zig can catch this kind of mistake and stop the program instead of silently producing bad behavior.

That is exactly what you want while developing.

#### Debug Builds Check Integer Overflow

Integer overflow happens when a number becomes too large for its type.

Example:

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

pub fn main() void {
    var x: u8 = 255;
    x += 1;

    std.debug.print("{}\n", .{x});
}
```

A `u8` can store values from `0` to `255`.

Adding `1` to `255` overflows.

In a debug build, Zig treats this as a safety problem. The program traps instead of quietly wrapping around.

That helps you find arithmetic bugs.

If you intentionally want wrapping arithmetic, Zig makes that explicit:

```zig
x +%= 1;
```

The `+%=` operator means wrapping addition assignment.

Zig’s design pushes you to write down your intent.

#### Debug Builds Check Invalid States

Zig has a special value called `unreachable`.

It means: “execution should never get here.”

Example:

```zig
fn digitName(digit: u8) []const u8 {
    return switch (digit) {
        0 => "zero",
        1 => "one",
        2 => "two",
        else => unreachable,
    };
}
```

This function only handles `0`, `1`, and `2`.

If someone calls:

```zig
_ = digitName(9);
```

then the program reaches `unreachable`.

In a debug build, this is caught. Zig reports that the program reached code marked as impossible.

This is useful, but use `unreachable` carefully. It is a promise to the compiler. Only write it when the code really should be impossible.

#### Debug Builds Give Better Stack Traces

A stack trace shows the path of function calls that led to a crash or failure.

Example:

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

fn third() void {
    unreachable;
}

fn second() void {
    third();
}

fn first() void {
    second();
}

pub fn main() void {
    first();
}
```

The call path is:

```text
main -> first -> second -> third
```

When the program fails, a debug build can show this path.

That tells you where the failure happened and how the program got there.

For beginners, stack traces are one of the most useful debugging tools. They turn “the program crashed” into “the program crashed here, through this sequence of calls.”

#### Debug Builds Are Slower

Debug builds do extra work.

They may check bounds, overflow, invalid enum values, invalid error states, and other safety conditions.

That extra work costs time.

So this is normal:

```text
Debug build: easier to debug, slower to run
Release build: harder to debug, faster to run
```

Do not judge final performance from a debug build.

Use debug builds while writing and testing code. Use release builds when measuring performance.

#### Building in Debug Mode

For a single file:

```bash
zig build-exe main.zig
```

This is enough for normal debugging.

You can run the result:

```bash
./main
```

On Windows:

```cmd
main.exe
```

For tests:

```bash
zig test main.zig
```

This runs tests in a debug-friendly mode by default.

#### Building in Release Modes

To build an optimized executable, use `-O`.

Example:

```bash
zig build-exe main.zig -O ReleaseFast
```

Other examples:

```bash
zig build-exe main.zig -O ReleaseSafe
zig build-exe main.zig -O ReleaseSmall
```

For tests:

```bash
zig test main.zig -O ReleaseSafe
```

This can be useful when you want to test behavior under an optimized but safety-checking build.

#### Debugging Tests

When a test fails, start with the failure message.

Example:

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

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

test "add returns the sum" {
    try std.testing.expectEqual(@as(i32, 7), add(3, 4));
}
```

The function is wrong. It subtracts instead of adding.

Run:

```bash
zig test main.zig
```

The test failure points you to the failing expectation.

Then inspect:

```zig
try std.testing.expectEqual(@as(i32, 7), add(3, 4));
```

The expected result is `7`. The actual result is `-1`.

From there, the bug is easy to find.

#### Add Temporary Debug Prints

You can use `std.debug.print` while debugging:

```zig
std.debug.print("value = {}\n", .{value});
```

Example:

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

fn factorial(n: u32) u32 {
    var result: u32 = 1;
    var i: u32 = 1;

    while (i <= n) : (i += 1) {
        std.debug.print("i = {}, result = {}\n", .{ i, result });
        result *= i;
    }

    return result;
}
```

This prints the state of the loop.

Debug prints are simple and effective. Remove them when they are no longer needed.

#### Prefer Small Reproducible Examples

When something fails, reduce the code.

Instead of debugging a large program all at once, create a smaller example that still fails.

Large bug:

```text
my whole parser crashes on one file
```

Smaller bug:

```text
this function crashes on this 8-byte input
```

Small examples are easier to inspect, test, and fix.

This is especially important in Zig because many bugs involve precise values: a length, an index, an allocator, a pointer, or a boundary.

#### Debug Builds and Undefined Values

Zig lets you write:

```zig
var x: i32 = undefined;
```

This means the variable has no meaningful value yet.

You must assign to it before reading it.

Bad:

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

pub fn main() void {
    var x: i32 = undefined;
    std.debug.print("{}\n", .{x});
}
```

Reading `x` before assigning a real value is a bug.

Debug builds can make such mistakes easier to notice, but the deeper rule is simpler:

Only use `undefined` when you are about to fully initialize the value before reading it.

For beginners, avoid `undefined` unless you have a clear reason.

#### Debug Builds and Allocators

Debugging memory bugs is easier when you use the right allocator.

In tests, use:

```zig
const allocator = std.testing.allocator;
```

Example:

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

test "allocate and free a buffer" {
    const allocator = std.testing.allocator;

    const buffer = try allocator.alloc(u8, 100);
    defer allocator.free(buffer);

    try std.testing.expectEqual(@as(usize, 100), buffer.len);
}
```

The testing allocator is designed to help tests catch allocation mistakes.

The important habit is this:

Every allocation needs a matching cleanup, unless ownership is clearly transferred somewhere else.

#### Debug First, Optimize Later

A common beginner mistake is to optimize too early.

Do not start with:

```bash
zig build-exe main.zig -O ReleaseFast
```

Start with:

```bash
zig test main.zig
```

and:

```bash
zig build-exe main.zig
```

Make the program correct first.

Then measure performance.

Then optimize the specific slow part.

This order matters. Fast wrong code is still wrong code.

#### Practical Workflow

A good beginner workflow is:

Write a small function.

Write a unit test.

Run `zig test`.

Fix compile errors.

Fix test failures.

Run the program in debug mode.

Add debug prints if needed.

Only later, try release builds.

For example:

```bash
zig test main.zig
zig build-exe main.zig
./main
zig build-exe main.zig -O ReleaseSafe
zig build-exe main.zig -O ReleaseFast
```

Use the safe modes to find bugs. Use the fast modes to measure final performance.

#### The Main Idea

Debug builds are not a beginner-only feature. Experienced Zig programmers rely on them too.

They make invalid behavior visible.

They catch many mistakes near the place where the mistake happens.

They give better information when something fails.

Use debug builds as your default working mode. Treat release builds as a separate step for shipping, benchmarking, and final testing.

