Skip to content

`switch` Expressions

A switch chooses one branch from several alternatives.

switch Expressions

A switch chooses one branch from several alternatives.

const name = switch (code) {
    200 => "ok",
    404 => "not found",
    500 => "server error",
    else => "unknown",
};

This is an expression. The value of the selected branch becomes the value of name.

The value being tested is called the switch operand.

switch (code) {
    ...
}

Each branch has one or more cases.

200 => "ok",

The value before => is matched against the operand. The expression after => is evaluated if the case matches.

Several cases can share one branch.

const kind = switch (ch) {
    'a', 'e', 'i', 'o', 'u' => "vowel",
    else => "other",
};

Ranges are allowed for integer and character values.

const class = switch (ch) {
    '0'...'9' => "digit",
    'a'...'z' => "lowercase",
    'A'...'Z' => "uppercase",
    else => "other",
};

The range includes both ends. The case '0'...'9' includes '0' and '9'.

A switch must be exhaustive. Every possible value must be handled.

For integers, this usually means using else.

const sign = switch (n) {
    0 => "zero",
    else => "nonzero",
};

For enums, else is often unnecessary because the compiler knows all cases.

const Color = enum {
    red,
    green,
    blue,
};

const text = switch (color) {
    .red => "red",
    .green => "green",
    .blue => "blue",
};

If a new enum value is added later, the compiler can point to every switch that must be updated. This is one of the main reasons to prefer a full enum switch over an else branch.

Branches may use blocks.

const price = switch (kind) {
    .small => 10,
    .large => blk: {
        const base = 20;
        const tax = 2;
        break :blk base + tax;
    },
};

The block computes the branch value and gives it back with break.

All branches must produce compatible types when the switch value is used.

const x = switch (n) {
    0 => 10,
    1 => 20,
    else => 30,
};

This is valid. Every branch gives an integer.

This is wrong:

const x = switch (n) {
    0 => 10,
    1 => "twenty",
    else => 30,
};

One branch gives a string. The others give integers. Zig rejects the program.

A switch can also be used only for control flow.

switch (mode) {
    .debug => std.debug.print("debug\n", .{}),
    .release => std.debug.print("release\n", .{}),
}

In this case each branch has type void.

switch works well with tagged unions. A tagged union stores one value chosen from a set of named alternatives.

const Token = union(enum) {
    number: i64,
    plus,
    minus,
};

fn printToken(tok: Token) void {
    switch (tok) {
        .number => |n| std.debug.print("number {d}\n", .{n}),
        .plus => std.debug.print("plus\n", .{}),
        .minus => std.debug.print("minus\n", .{}),
    }
}

The branch

.number => |n| ...

matches the number case and captures its payload in n.

This is safer than storing a tag and a value separately. The compiler keeps the tag and payload connected.

A switch can also capture the matched value.

const text = switch (n) {
    0 => "zero",
    1, 2, 3 => |x| blk: {
        _ = x;
        break :blk "small";
    },
    else => "large",
};

The captured value is the value that matched the case.

Use switch when the shape of the decision matters. Use if when the decision is just a boolean test.

const result = if (n < 10) "small" else "large";

This is better as if.

const result = switch (status) {
    .open => "open",
    .closed => "closed",
    .unknown => "unknown",
};

This is better as switch.

Exercise 3-9. Write a switch that converts digits 0 through 9 to their English names.

Exercise 3-10. Write a switch over an enum named Direction with cases north, south, east, and west.

Exercise 3-11. Add a new enum case and observe which switches must change.

Exercise 3-12. Write a tagged union for a simple token: integer, left parenthesis, right parenthesis, plus, and minus. Use switch to print each token.