# Unreachable Code

### Unreachable Code

Some parts of a program should never run.

For example:

```zig
switch (status) {
    .ok => handleOk(),
    .failed => handleFailed(),
}
```

If every possible `status` value is handled, there should be no unknown case.

But sometimes you need to tell Zig:

this point should be impossible to reach.

Zig gives you `unreachable` for that.

```zig
unreachable;
```

`unreachable` means: if the program reaches this point, something is wrong.

#### A First Example

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

pub fn main() void {
    const x = 10;

    if (x > 0) {
        std.debug.print("positive\n", .{});
    } else {
        unreachable;
    }
}
```

This prints:

```text
positive
```

The `else` branch says:

```zig
unreachable;
```

That means the programmer believes this branch cannot happen.

In this exact program, `x` is always `10`, so the `else` branch really is impossible.

#### `unreachable` Is a Promise

`unreachable` is not just a comment.

It is a promise to the compiler.

You are saying:

this code path cannot happen.

That promise matters. In safe build modes, reaching `unreachable` causes a runtime safety panic. In optimized builds, the compiler may assume it cannot happen and use that assumption for optimization.

So you should not use `unreachable` casually.

This is dangerous:

```zig
fn printPositive(x: i32) void {
    if (x > 0) {
        std.debug.print("{}\n", .{x});
    } else {
        unreachable;
    }
}
```

The function name says `printPositive`, but the parameter type is still `i32`. A caller can pass `0` or `-5`.

```zig
printPositive(-5);
```

Then the supposedly impossible branch is reached.

The better version checks the input properly:

```zig
fn printPositive(x: i32) void {
    if (x <= 0) {
        std.debug.print("not positive\n", .{});
        return;
    }

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

Use `unreachable` only when the program logic truly makes a path impossible.

#### `unreachable` After an Infinite Loop

Sometimes a function has a return type, but the code never returns normally.

```zig
fn runForever() noreturn {
    while (true) {
        // keep running
    }
}
```

The return type `noreturn` means this function never returns to its caller.

A simpler example:

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

fn crash() noreturn {
    @panic("stop");
}

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

`@panic` does not return. It stops the program.

`unreachable` also has type `noreturn`. That means it fits anywhere a value is expected, because execution never continues past it.

#### `unreachable` in `switch`

One common place to see `unreachable` is in a `switch`.

Example:

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

fn digitName(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,
    };
}

pub fn main() void {
    std.debug.print("{s}\n", .{digitName(3)});
}
```

This prints:

```text
three
```

But this function is not fully safe:

```zig
digitName(99);
```

The type `u8` allows values from `0` to `255`. So the `else` branch can happen.

A better design is to use a smaller type or handle invalid input.

```zig
fn digitName(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 => "not a digit",
    };
}
```

Now the function handles all valid `u8` inputs.

#### Better: Make Impossible States Impossible

Instead of using `unreachable`, often you should design the type better.

For example, this function accepts any `u8`:

```zig
fn digitName(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,
    };
}
```

But only ten values are meaningful.

An enum can model the valid cases directly:

```zig
const Digit = enum {
    zero,
    one,
    two,
    three,
    four,
    five,
    six,
    seven,
    eight,
    nine,
};

fn digitName(digit: Digit) []const u8 {
    return switch (digit) {
        .zero => "zero",
        .one => "one",
        .two => "two",
        .three => "three",
        .four => "four",
        .five => "five",
        .six => "six",
        .seven => "seven",
        .eight => "eight",
        .nine => "nine",
    };
}
```

Now there is no invalid digit value.

The `switch` is exhaustive. No `else` is needed.

This is better Zig design: do not accept impossible states when the type system can prevent them.

#### `unreachable` and Exhaustive Logic

Sometimes `unreachable` appears after a loop that should always return.

```zig
fn findFirstEven(numbers: []const u8) u8 {
    for (numbers) |n| {
        if (n % 2 == 0) {
            return n;
        }
    }

    unreachable;
}
```

This function says:

there is always at least one even number.

But the type does not prove that.

A caller can pass:

```zig
&[_]u8{ 1, 3, 5 }
```

Then the loop finishes and reaches `unreachable`.

A better function returns an optional:

```zig
fn findFirstEven(numbers: []const u8) ?u8 {
    for (numbers) |n| {
        if (n % 2 == 0) {
            return n;
        }
    }

    return null;
}
```

The return type `?u8` says:

this function may return a number, or it may return nothing.

That is clearer and safer.

#### `unreachable` for Proven Impossible Branches

There are cases where `unreachable` is reasonable.

Suppose a value has already been checked.

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

fn divideByNonZero(a: i32, b: i32) i32 {
    if (b == 0) {
        @panic("b must not be zero");
    }

    if (b != 0) {
        return @divTrunc(a, b);
    } else {
        unreachable;
    }
}

pub fn main() void {
    std.debug.print("{}\n", .{divideByNonZero(10, 2)});
}
```

After this check:

```zig
if (b == 0) {
    @panic("b must not be zero");
}
```

the later `else` branch should be impossible.

Even here, the code can be simpler:

```zig
fn divideByNonZero(a: i32, b: i32) i32 {
    if (b == 0) {
        @panic("b must not be zero");
    }

    return @divTrunc(a, b);
}
```

The simpler version is better. It avoids needing `unreachable` at all.

#### `unreachable` in Low-Level Code

Low-level Zig code sometimes uses `unreachable` when dealing with things outside Zig’s type system:

C APIs

hardware registers

system calls

compiler internals

manual state machines

For example, a C library may return integer codes:

```zig
const Code = enum(i32) {
    ok = 0,
    failed = 1,
};
```

If an external function is documented to return only `0` or `1`, you may see code that treats other values as impossible.

```zig
fn decode(code: i32) Code {
    return switch (code) {
        0 => .ok,
        1 => .failed,
        else => unreachable,
    };
}
```

But be careful. External systems can violate expectations. In many cases, returning an error is safer:

```zig
const DecodeError = error{
    UnknownCode,
};

fn decode(code: i32) DecodeError!Code {
    return switch (code) {
        0 => .ok,
        1 => .failed,
        else => error.UnknownCode,
    };
}
```

This version admits reality: external input may be invalid.

#### `unreachable` as Documentation

`unreachable` documents an invariant.

An invariant is a rule that should always be true at a certain point in the program.

Example:

```zig
if (state == .closed) {
    unreachable;
}
```

This says:

at this point, `state` should never be `.closed`.

But `unreachable` is stronger than a comment. It changes program behavior if the invariant is broken.

If the invariant can be broken by user input, file contents, network data, or external APIs, do not use `unreachable`. Handle the case.

Use `unreachable` for internal logic that truly cannot happen when the program is correct.

#### `unreachable` vs `@panic`

Both can stop the program.

```zig
unreachable;
```

means:

this code path is impossible.

```zig
@panic("message");
```

means:

this code path is possible, but it is a fatal error.

Use `@panic` when you want to report a deliberate fatal failure.

Use `unreachable` when reaching the code means the program’s internal reasoning is broken.

Example:

```zig
if (config_is_invalid) {
    @panic("invalid hard-coded config");
}
```

This is a panic because the case is possible, but fatal.

Example:

```zig
switch (mode) {
    .read => read(),
    .write => write(),
}
```

No `unreachable` is needed when the switch is already exhaustive.

#### Avoid Using `unreachable` to Silence the Compiler

A bad use of `unreachable` is forcing code to compile when the type design is wrong.

Bad:

```zig
fn getFirst(items: []const u8) u8 {
    if (items.len > 0) {
        return items[0];
    }

    unreachable;
}
```

The function accepts an empty slice, so the empty case is reachable.

Better:

```zig
fn getFirst(items: []const u8) ?u8 {
    if (items.len > 0) {
        return items[0];
    }

    return null;
}
```

Or, if empty input is a programmer error, make that clear:

```zig
fn getFirstNonEmpty(items: []const u8) u8 {
    std.debug.assert(items.len > 0);
    return items[0];
}
```

The name and assertion communicate the requirement.

#### The Main Idea

`unreachable` marks a code path that should be impossible.

It is useful, but sharp.

Use it when the program logic or type system has already ruled out a path.

Do not use it for bad input, missing files, network errors, invalid user data, or normal failure cases.

The beginner rule is simple:

prefer better types, explicit errors, optionals, or assertions.

Use `unreachable` only when reaching that line would mean the program itself is wrong.

