Skip to content

`switch`

An if statement is good for general conditions:

switch

An if statement is good for general conditions:

if (age >= 18) {
    // ...
}

But sometimes you are not testing ranges or complex logic. You are matching one value against several exact cases.

For example:

  • a menu choice
  • a command name
  • a keyboard key
  • an enum value
  • a token type in a parser

This is where switch is useful.

A switch compares one value against multiple possible patterns.

Your First switch

const std = @import("std");

pub fn main() void {
    const day = 3;

    switch (day) {
        1 => std.debug.print("Monday\n", .{}),
        2 => std.debug.print("Tuesday\n", .{}),
        3 => std.debug.print("Wednesday\n", .{}),
        else => std.debug.print("Unknown day\n", .{}),
    }
}

This program prints:

Wednesday

The value being checked is:

day

Zig compares it against each case:

1 => ...
2 => ...
3 => ...

When Zig finds a matching case, it runs that branch.

If nothing matches, the else branch runs.

The Shape of a switch

The general shape is:

switch (value) {
    pattern1 => expression1,
    pattern2 => expression2,
    else => fallback_expression,
}

Each branch uses:

pattern => result

The arrow => means “if this pattern matches, use this result.”

switch Is an Expression

Like if, switch can produce a value.

const std = @import("std");

pub fn main() void {
    const day = 2;

    const name = switch (day) {
        1 => "Monday",
        2 => "Tuesday",
        3 => "Wednesday",
        else => "Unknown",
    };

    std.debug.print("{s}\n", .{name});
}

This prints:

Tuesday

The whole switch expression becomes one value.

If day is 2, the result becomes:

"Tuesday"

That value is stored in name.

This style is very common in Zig.

switch Does Not Fall Through

In C, switch statements can “fall through” accidentally.

Example C code:

switch (x) {
    case 1:
        printf("one\n");

    case 2:
        printf("two\n");
}

If x is 1, this prints both:

one
two

because execution falls into the next case unless you write break.

Zig does not work this way.

In Zig, each branch is separate and isolated.

switch (x) {
    1 => std.debug.print("one\n", .{}),
    2 => std.debug.print("two\n", .{}),
    else => {},
}

Only one branch runs.

This removes a very common source of bugs.

Multiple Patterns in One Branch

You can combine several patterns.

const std = @import("std");

pub fn main() void {
    const c = 'a';

    switch (c) {
        'a', 'e', 'i', 'o', 'u' => {
            std.debug.print("vowel\n", .{});
        },
        else => {
            std.debug.print("not vowel\n", .{});
        },
    }
}

This prints:

vowel

The branch runs if any listed pattern matches.

Ranges in switch

You can also match ranges.

const std = @import("std");

pub fn main() void {
    const score = 85;

    switch (score) {
        90...100 => std.debug.print("grade A\n", .{}),
        80...89 => std.debug.print("grade B\n", .{}),
        70...79 => std.debug.print("grade C\n", .{}),
        else => std.debug.print("lower grade\n", .{}),
    }
}

This prints:

grade B

The syntax:

80...89

means “all integers from 80 through 89 inclusive.”

Ranges make some switch statements much cleaner than long if else if chains.

Blocks Inside Branches

A branch can contain a block.

const std = @import("std");

pub fn main() void {
    const command = 1;

    switch (command) {
        1 => {
            std.debug.print("open file\n", .{});
            std.debug.print("loading data\n", .{});
        },
        2 => {
            std.debug.print("save file\n", .{});
        },
        else => {
            std.debug.print("unknown command\n", .{});
        },
    }
}

Use blocks when the branch needs multiple statements.

Matching Enums

switch becomes especially powerful with enums.

const std = @import("std");

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

pub fn main() void {
    const dir = Direction.east;

    switch (dir) {
        .north => std.debug.print("up\n", .{}),
        .south => std.debug.print("down\n", .{}),
        .east => std.debug.print("right\n", .{}),
        .west => std.debug.print("left\n", .{}),
    }
}

This prints:

right

Notice the enum values:

.north
.south
.east
.west

Inside the switch, Zig already knows the type is Direction, so the enum name can be omitted.

Exhaustive Checking

One of Zig’s best features is exhaustive checking.

If every enum case is handled, Zig knows the code is complete.

Example:

const Direction = enum {
    north,
    south,
};

fn move(dir: Direction) void {
    switch (dir) {
        .north => {},
        .south => {},
    }
}

This is complete.

But suppose you later add:

east

Now the compiler reports an error because the switch no longer handles every possibility.

This is extremely valuable in large programs. The compiler helps you update all affected code safely.

Using else

You can use else as a fallback branch.

switch (value) {
    1 => {},
    2 => {},
    else => {},
}

But when switching over enums, many Zig programmers prefer exhaustive handling without else.

Why?

Because if you add a new enum value later, the compiler forces you to update the switch.

Example:

const State = enum {
    idle,
    running,
    stopped,
};

switch (state) {
    .idle => {},
    .running => {},
    .stopped => {},
}

This is safer than:

switch (state) {
    .idle => {},
    else => {},
}

The second version silently hides future enum values inside else.

Capturing Values

switch can capture matched values.

const std = @import("std");

const Token = union(enum) {
    number: i32,
    word: []const u8,
};

pub fn main() void {
    const token = Token{ .number = 42 };

    switch (token) {
        .number => |n| {
            std.debug.print("number: {}\n", .{n});
        },
        .word => |w| {
            std.debug.print("word: {s}\n", .{w});
        },
    }
}

This prints:

number: 42

This syntax:

.number => |n|

means:

  • match the .number case
  • extract the stored value
  • name it n

This feature is very important when working with tagged unions.

switch and Types

All branches in a value-returning switch must produce compatible types.

This is valid:

const result = switch (x) {
    1 => "one",
    2 => "two",
    else => "other",
};

All branches return strings.

This is invalid:

const result = switch (x) {
    1 => 123,
    else => "hello",
};

One branch returns an integer. The other returns a string.

Zig requires a consistent result type.

switch vs if

Use if when:

  • conditions are unrelated
  • conditions involve comparisons
  • conditions involve boolean logic

Example:

if (x > 0 and x < 100) {
    // ...
}

Use switch when:

  • matching one value
  • handling enums
  • handling many exact cases
  • matching ranges cleanly

Example:

switch (command) {
    .open => {},
    .save => {},
    .quit => {},
}

Empty Branches

Sometimes a branch intentionally does nothing.

switch (value) {
    0 => {},
    else => std.debug.print("not zero\n", .{}),
}

The empty block:

{}

means “do nothing.”

The Main Idea

A switch compares one value against multiple patterns.

It is cleaner and safer than long chains of if else if when many exact cases exist.

Zig’s switch has several important properties:

  • no accidental fallthrough
  • exhaustive enum checking
  • ranges and multiple patterns
  • value-returning expressions
  • pattern matching with captured values

You will use switch constantly in real Zig programs, especially when working with enums, parsers, protocols, compilers, and state machines.