# Panic and Crash Behavior

### Panic and Crash Behavior

Errors are for expected failures.

Panics are for broken assumptions.

This is the most important difference.

A missing file can be an error. Invalid user input can be an error. A network timeout can be an error. These are normal things that can happen while the program is running.

But an out-of-bounds array index, an impossible state, or a violated invariant usually means the program logic is wrong.

That is where panic behavior matters.

### Error vs Panic

Use an error when the caller can reasonably handle the problem.

```zig
fn openConfig() !std.fs.File {
    return std.fs.cwd().openFile("config.json", .{});
}
```

The file may be missing. The caller can decide what to do.

Use a panic when the program has reached a state that should not be possible.

```zig
fn getFirst(items: []const u8) u8 {
    if (items.len == 0) {
        @panic("getFirst called with empty slice");
    }

    return items[0];
}
```

Here, the function expects a non-empty slice. If it receives an empty slice, the caller violated the function’s contract.

### What Is a Panic

A panic means the program stops because something is seriously wrong.

You can trigger one directly:

```zig
@panic("something went wrong");
```

This is not normal error handling. It is a hard failure.

The program is saying:

```text
I cannot safely continue from here.
```

A panic is useful when continuing would be more dangerous than stopping.

### Common Causes of Panics

In safe build modes, Zig can detect many programmer mistakes.

Examples include:

```text
array index out of bounds
integer overflow
division by zero
reaching unreachable code
invalid enum value
failed safety checks
```

For example:

```zig
const numbers = [_]u8{ 10, 20, 30 };

pub fn main() void {
    const x = numbers[10];
    _ = x;
}
```

The array has only three elements. Index `10` is invalid.

This is not an ordinary runtime failure like a missing file. It is a bug in the program.

### `unreachable`

Zig has a special keyword:

```zig
unreachable
```

It means:

```text
execution must never reach this point
```

Example:

```zig
fn describeDigit(n: u8) []const u8 {
    return switch (n) {
        0 => "zero",
        1 => "one",
        2 => "two",
        3 => "three",
        4 => "four",
        5 => "five",
        6 => "six",
        7 => "seven",
        8 => "eight",
        9 => "nine",
        else => unreachable,
    };
}
```

This code claims that the `else` branch cannot happen.

But the claim is false. `n` is a `u8`, so it can be `10`, `200`, or `255`.

A better version would return an error:

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

fn describeDigit(n: u8) DigitError![]const u8 {
    return switch (n) {
        0 => "zero",
        1 => "one",
        2 => "two",
        3 => "three",
        4 => "four",
        5 => "five",
        6 => "six",
        7 => "seven",
        8 => "eight",
        9 => "nine",
        else => error.NotADigit,
    };
}
```

Use `unreachable` only when the branch is truly impossible.

### `catch unreachable`

You will often see this pattern:

```zig
const value = parseDigit(c) catch unreachable;
```

This means:

```text
I know this function cannot fail here.
If it does fail, the program has a bug.
```

Example:

```zig
fn parseDigit(c: u8) !u8 {
    if (c < '0' or c > '9') {
        return error.InvalidDigit;
    }

    return c - '0';
}

fn parseKnownDigit(c: u8) u8 {
    if (c < '0' or c > '9') {
        return 0;
    }

    return parseDigit(c) catch unreachable;
}
```

The caller checks the input before calling `parseDigit`.

So inside `parseKnownDigit`, failure should be impossible.

Still, this pattern should be used carefully. `catch unreachable` is a strong promise. If future code changes break the promise, the program may crash.

### Panic Is Not a Replacement for Errors

Do not use panic for normal failures.

This is poor API design:

```zig
fn readConfig() []const u8 {
    const file = std.fs.cwd().openFile("config.json", .{}) catch {
        @panic("could not open config");
    };

    _ = file;
    return "config";
}
```

The caller has no chance to handle the missing file.

A better API returns an error:

```zig
fn readConfig() ![]const u8 {
    const file = try std.fs.cwd().openFile("config.json", .{});
    _ = file;

    return "config";
}
```

Now the caller can choose:

```zig
const config = readConfig() catch |err| switch (err) {
    error.FileNotFound => "default config",
    else => return err,
};
```

That is the Zig style. Expected failures should be returned, not hidden behind panics.

### When Panic Is Reasonable

Panic is reasonable when the program cannot continue safely.

Examples:

```text
an internal invariant is broken
a supposedly impossible branch is reached
a required compile-time assumption fails at runtime
a function is called with invalid arguments that violate its contract
critical initialization cannot continue in a small program
```

For example:

```zig
fn setMode(mode: Mode) void {
    switch (mode) {
        .debug => enableDebugMode(),
        .release => enableReleaseMode(),
    }
}
```

If `Mode` only has `.debug` and `.release`, there is no need for a default branch. The type system covers all cases.

But if you use `unreachable`, make sure the type really proves the case impossible.

### Panic in Small Programs

For small scripts or experiments, panicking may be acceptable.

```zig
pub fn main() void {
    const allocator = std.heap.page_allocator;

    const data = std.fs.cwd().readFileAlloc(
        allocator,
        "input.txt",
        1024 * 1024,
    ) catch @panic("failed to read input.txt");

    defer allocator.free(data);

    // use data
}
```

This is simple and direct.

But for libraries and serious applications, return errors instead. A library should not usually decide to crash the caller’s program.

### Libraries Should Prefer Errors

A library function should usually expose failure to the caller.

Poor library behavior:

```zig
pub fn parse(text: []const u8) Ast {
    if (text.len == 0) {
        @panic("empty input");
    }

    // ...
}
```

Better:

```zig
const ParseError = error{
    EmptyInput,
    InvalidSyntax,
};

pub fn parse(text: []const u8) ParseError!Ast {
    if (text.len == 0) {
        return error.EmptyInput;
    }

    // ...
}
```

The library does not know the caller’s policy.

One application may treat empty input as an error.

Another may show a warning.

Another may insert a default value.

Returning an error keeps that choice with the caller.

### Build Modes Matter

Zig has different build modes.

The exact behavior of safety checks can depend on the build mode. Debug and safe release builds preserve runtime safety checks. Fast builds may remove some checks for performance.

This affects bugs like:

```text
integer overflow
out-of-bounds access
reaching unreachable
```

The lesson for beginners is simple: do not depend on safety checks as your program logic.

Write correct code first. Use safety checks as a guardrail, not as the design.

### Crash Early, Not Corrupt Silently

When a program has a serious internal bug, stopping can be better than continuing.

Continuing after memory corruption or broken invariants may produce worse results:

```text
wrong output
corrupted files
security bugs
hard-to-debug failures later
```

A panic gives a clear failure point.

This is one reason Zig includes runtime safety checks in safe modes. It is better to find the bug close to where it happens.

### The Core Rule

Use errors for expected failure.

Use panics for broken assumptions.

Use `unreachable` only when a branch is truly impossible.

Use `catch unreachable` only when you have already proven the error cannot happen.

A good Zig program does not panic because a user typed bad input or a file was missing. It returns errors for those cases.

A good Zig program panics when the program itself has violated its own rules.

