⚡ Zig Guide LiveUnofficialbut fully verified
✓ Zig 0.17.0-dev.2122+3e15e99e6What's newOn an older Zig?

Switch

const std = @import("std");
const expect = std.testing.expect;

test "switch as a statement" {
    var x: i8 = 10;
    switch (x) {
        -1...1 => x = -x, // inclusive ranges
        10, 100 => x = @divExact(x, 10), // multiple values
        else => {},
    }
    try expect(x == 1);
}

test "switch as an expression" {
    const x: i8 = 10;
    const y: i8 = switch (x) {
        -1...1 => -x,
        10, 100 => @divExact(x, 10),
        else => 0,
    };
    try expect(y == 1);
}

const Direction = enum { north, south, east, west };

fn isVertical(d: Direction) bool {
    // No `else` branch: the compiler proves every case is handled, so adding
    // a new enum tag later turns this into a compile error rather than a bug.
    return switch (d) {
        .north, .south => true,
        .east, .west => false,
    };
}

test "exhaustive switch over an enum" {
    try expect(isVertical(.north));
    try expect(!isVertical(.east));
}

Zig’s switch has no fallthrough, and it must be exhaustive: every possible value needs a branch, or the program does not compile.

No fallthrough means no break at the end of every arm, and no bug from forgetting one. An arm that should cover several values lists them, which is the thing fallthrough was usually being used for anyway.

Ranges and multiple values

switch (x) {
    -1...1 => ...,      // inclusive range
    10, 100 => ...,     // several values, one branch
    else => ...,
}

Note ... for ranges is inclusive on both ends, unlike the .. used in for ranges, which excludes the upper bound. That difference is deliberate and it trips people up: 0..3 in a for visits three values, 0...3 in a switch matches four.

Cases must not overlap. Two arms that can both match the same value are a compile error rather than a first-match-wins rule, so the order arms are written in never changes behaviour.

A statement or an expression

Both forms are in the snippet. As a statement each arm does something; as an expression each arm produces a value and the whole switch is that value:

const y: i8 = switch (x) {
    -1...1 => -x,
    10, 100 => @divExact(x, 10),
    else => 0,
};

Prefer the expression form where it fits, for the same reason as loops as expressions. The result goes straight into a const, and every arm has to produce one.

The exhaustiveness payoff

When switching over an enum and covering every tag, leave out else:

return switch (direction) {
    .north, .south => true,
    .east, .west => false,
};

Now adding a .up variant later turns this into a compile error pointing straight at the code that needs updating. Adding else => false would instead silently classify the new direction as horizontal. The else branch is a convenience that costs you the compiler’s help. Spend it deliberately.

The error names the tag you missed and points at its declaration:

error: switch must handle all possibilities
note: unhandled enumeration value: 'west'

For an integer, “all possibilities” means all of them. A switch on a u8 needs else or 256 arms, which is why else is normal there and suspicious on an enum.

Switching on a tagged union

Each arm can capture the payload of the variant it matched:

switch (value) {
    .int => |n| useInt(n),
    .text => |s| useText(s),
}

Capture by reference with |*n| when the arm needs to modify it. This is the main way tagged unions are consumed and it is covered in unions and payload captures.

inline else and non-exhaustive enums

Two forms worth knowing exist once you are generating code rather than writing it out.

inline else compiles a separate copy of the arm for every remaining case, with the tag available as a comptime value:

switch (d) {
    inline else => |tag| doSomething(@tagName(tag)),
}

Ordinary else cannot do that, because at runtime the tag is just a number. The inline form is how reflection-style code stays type-safe.

An enum declared with a trailing _ is non-exhaustive: values outside its named tags are legal, usually because the numbers come from a protocol or a C header. Those switch on _ rather than else:

const Op = enum(u8) { get = 1, set = 2, _ };

switch (op) {
    .get => ...,
    .set => ...,
    _ => return error.UnknownOpcode,
}

_ means “a value with no name”. It keeps the named-tag exhaustiveness check working: add .delete to the enum and this switch still fails to compile until you handle it. else would have swallowed it.