# Stack Traces

### Stack Traces

A stack trace shows how your program reached a failure.

When a program crashes, the most important question is not only “what failed?” It is also “how did the program get there?” A stack trace answers that second question.

#### A Simple Crash

Look at this program:

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

fn third() void {
    unreachable;
}

fn second() void {
    third();
}

fn first() void {
    second();
}

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

The call chain is:

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

The failure happens in `third`, because `unreachable` was reached.

A stack trace helps you see that path.

#### What the Stack Is

When a function calls another function, Zig must remember where to return later.

For this code:

```zig
fn first() void {
    second();
}
```

the program enters `first`, then calls `second`.

While `second` runs, `first` is still waiting.

If `second` calls `third`, then both `first` and `second` are waiting.

This creates a stack of active function calls.

A stack trace is a printed view of that stack.

#### Reading a Stack Trace

A stack trace usually contains function names and source locations.

You might see information like:

```text
third
second
first
main
```

Read it from the failure outward:

`third` failed.

`second` called `third`.

`first` called `second`.

`main` called `first`.

That tells you where to start looking.

#### Stack Traces and Tests

Stack traces are especially useful when a test fails deep inside helper code.

Example:

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

fn requirePositive(n: i32) void {
    if (n <= 0) unreachable;
}

fn calculate(n: i32) i32 {
    requirePositive(n);
    return n * 2;
}

test "calculate doubles the input" {
    try std.testing.expectEqual(@as(i32, 10), calculate(0));
}
```

The test says it expects `10`, but it calls `calculate(0)`.

Inside `calculate`, the helper function rejects zero.

The useful question is:

Which test caused `requirePositive` to fail?

A stack trace points from the failure back to the test block.

#### Do Not Stop at the Top Frame

The first frame tells you where the crash happened.

It may not tell you where the bug is.

Example:

```zig
fn getAt(items: []const i32, index: usize) i32 {
    return items[index];
}
```

If this function crashes because `index` is out of bounds, `getAt` may appear at the top of the stack trace.

But the real bug may be in the caller that passed the wrong index.

So read several frames, not just the first one.

Ask:

Who called this function?

What arguments did it pass?

Which earlier decision made those arguments possible?

#### Stack Traces and Bounds Errors

A common beginner bug is an invalid index.

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

fn last(items: []const i32) i32 {
    return items[items.len];
}

test "last returns the final item" {
    const values = [_]i32{ 10, 20, 30 };
    try std.testing.expectEqual(@as(i32, 30), last(values[0..]));
}
```

The valid indexes are `0`, `1`, and `2`.

But `items.len` is `3`.

So this line is wrong:

```zig
return items[items.len];
```

The last valid index is:

```zig
items.len - 1
```

The corrected function is:

```zig
fn last(items: []const i32) i32 {
    return items[items.len - 1];
}
```

A stack trace can show the failing line. Your job is to understand the logic around that line.

#### Stack Traces and Error Returns

Not every failure is a crash.

Sometimes a test fails because a function returned an error.

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

fn parsePort(text: []const u8) !u16 {
    return try std.fmt.parseInt(u16, text, 10);
}

test "parsePort parses port numbers" {
    const port = try parsePort("abc");
    try std.testing.expectEqual(@as(u16, 8080), port);
}
```

Here, `"abc"` cannot be parsed as a number.

The test fails at:

```zig
const port = try parsePort("abc");
```

A stack trace or test failure output helps you locate the failing `try`.

Then you inspect the input.

The bug may be the function. It may also be the test. In this case, the test gives invalid input but expects success.

#### Add Context with Debug Prints

A stack trace gives call locations.

It does not always show the values that caused the failure.

Add temporary prints:

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

Example:

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

fn getAt(items: []const i32, index: usize) i32 {
    std.debug.print("index = {}, len = {}\n", .{ index, items.len });
    return items[index];
}
```

Now, when the program fails, you can see the bad values.

Remove temporary debug prints after fixing the bug.

#### Keep Functions Small

Stack traces are easier to read when functions are small.

If a stack trace points to a 200-line function, you still have a lot of work.

If it points to a 10-line function, the bug is easier to isolate.

This is a practical reason to keep code divided into small functions. Small functions make testing easier, but they also make debugging easier.

#### Stack Traces and `unreachable`

`unreachable` means “this code path should never happen.”

Example:

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

If you call:

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

then the program reaches `unreachable`.

A stack trace shows where the bad value came from.

But the deeper fix is often to remove the invalid assumption.

Maybe the function should return an error instead:

```zig
const DigitError = error{
    InvalidDigit,
};

fn nameOfDigit(digit: u8) ![]const u8 {
    return switch (digit) {
        0 => "zero",
        1 => "one",
        2 => "two",
        else => DigitError.InvalidDigit,
    };
}
```

Use `unreachable` only when the case truly cannot happen.

#### Stack Traces and Optimization

Debug builds usually give the clearest stack traces.

Optimized release builds can inline functions, remove information, or reorder code. That can make stack traces harder to read.

When debugging, start with:

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

or:

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

Do not start with:

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

First make the bug reproducible in debug mode. Then fix it.

#### A Good Debugging Process

When you see a stack trace, use this process:

Read the top frame.

Find the source line.

Read the next few frames.

Identify the caller.

Inspect the arguments.

Add debug prints if needed.

Write a small test that reproduces the bug.

Fix the code.

Keep the test.

The final step matters. Once you find a bug, turn it into a test so it does not return later.

#### Complete Example

Save this as `main.zig`:

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

fn getLast(items: []const i32) i32 {
    return items[items.len];
}

fn summarize(items: []const i32) i32 {
    return getLast(items);
}

test "summarize returns the last item" {
    const values = [_]i32{ 10, 20, 30 };
    try std.testing.expectEqual(@as(i32, 30), summarize(values[0..]));
}
```

Run:

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

The test fails because `getLast` uses an invalid index.

Fix it:

```zig
fn getLast(items: []const i32) i32 {
    return items[items.len - 1];
}
```

Then run the test again.

The main lesson is simple: a stack trace is not noise. It is a map. It shows the route your program took before it failed. Read it from the failure back through the callers, then use that path to find the wrong assumption.

