Skip to content

Enums

An enum is a type whose value must be one item from a fixed list.

An enum is a type whose value must be one item from a fixed list.

Use an enum when a value has a small number of named choices.

For example, a traffic light can be red, yellow, or green:

const TrafficLight = enum {
    red,
    yellow,
    green,
};

This defines a new type named TrafficLight.

A value of this type can only be one of these:

TrafficLight.red
TrafficLight.yellow
TrafficLight.green

It cannot be some random string or number.

Creating Enum Values

You can create an enum value by writing the enum type, a dot, and the tag name:

const light = TrafficLight.red;

Here, light has the value red.

When the expected type is already known, Zig lets you use a shorter form:

const light: TrafficLight = .red;

The type annotation tells Zig that .red means TrafficLight.red.

This shorter style is common in Zig.

Why Enums Are Useful

Without enums, you might write code like this:

const status = 2;

But what does 2 mean?

Does it mean active? Failed? Pending? Closed?

An enum gives the value a name:

const Status = enum {
    pending,
    active,
    failed,
    closed,
};

const status = Status.active;

Now the code explains itself.

The compiler also checks that you only use valid values.

Switching on an Enum

Enums work well with switch.

const std = @import("std");

const Status = enum {
    pending,
    active,
    failed,
    closed,
};

pub fn main() void {
    const status: Status = .active;

    switch (status) {
        .pending => std.debug.print("pending\n", .{}),
        .active => std.debug.print("active\n", .{}),
        .failed => std.debug.print("failed\n", .{}),
        .closed => std.debug.print("closed\n", .{}),
    }
}

Output:

active

The switch handles every possible enum tag.

That is one of the main benefits. If you later add a new enum tag, Zig can force you to update the switch.

Exhaustive Switches

An exhaustive switch handles every possible value.

switch (status) {
    .pending => {},
    .active => {},
    .failed => {},
    .closed => {},
}

Because Status has exactly four possible values, the switch must cover all four.

This is safer than using loose integers or strings. The compiler knows the full list of possible values.

You can also use else:

switch (status) {
    .active => std.debug.print("active\n", .{}),
    else => std.debug.print("not active\n", .{}),
}

Use else when you really want to group the remaining cases together. For important state machines, explicit cases are usually better.

Enum Methods

Like structs, enums can contain functions.

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

    fn isVertical(self: Direction) bool {
        return switch (self) {
            .north, .south => true,
            .east, .west => false,
        };
    }
};

Use it like this:

const d: Direction = .north;
const vertical = d.isVertical();

The method receives self: Direction.

This works the same way as struct methods. The call:

d.isVertical()

is shorthand for:

Direction.isVertical(d)

A Complete Enum Example

const std = @import("std");

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

    fn opposite(self: Direction) Direction {
        return switch (self) {
            .north => .south,
            .south => .north,
            .east => .west,
            .west => .east,
        };
    }
};

pub fn main() void {
    const d: Direction = .north;
    const other = d.opposite();

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

This prints the opposite direction.

The important part is this method:

fn opposite(self: Direction) Direction {
    return switch (self) {
        .north => .south,
        .south => .north,
        .east => .west,
        .west => .east,
    };
}

It takes one Direction and returns another Direction.

Enum Values Are Not Strings

This enum tag:

.red

is not the string "red".

It is a typed enum value.

That means this is wrong:

const color: Color = "red"; // error

You must use the enum tag:

const color: Color = .red;

This distinction matters. Strings are text. Enums are fixed symbolic values known to the compiler.

Enum Values Are Not Plain Integers

Enums may have integer representations internally, but you should not treat them as plain integers in normal code.

This is the right style:

const mode: Mode = .read;

This is usually the wrong style:

const mode = 0;

The second version loses meaning and type safety.

When you need explicit integer values, Zig supports that too.

Enums with Explicit Integer Values

You can choose the integer tag type:

const HttpStatus = enum(u16) {
    ok = 200,
    not_found = 404,
    internal_server_error = 500,
};

Here, the enum uses u16 as its tag type.

Each tag has a numeric value:

ok = 200
not_found = 404
internal_server_error = 500

This is useful when the numbers are part of an external format, protocol, ABI, or file format.

Example:

const status: HttpStatus = .not_found;

The enum value is still not just a raw u16. It is an HttpStatus.

Converting Enums to Integers

You can convert an enum value to its integer tag with @intFromEnum.

const code = @intFromEnum(HttpStatus.not_found);

Now code is 404.

Complete example:

const std = @import("std");

const HttpStatus = enum(u16) {
    ok = 200,
    not_found = 404,
    internal_server_error = 500,
};

pub fn main() void {
    const status: HttpStatus = .not_found;
    const code = @intFromEnum(status);

    std.debug.print("code = {}\n", .{code});
}

Output:

code = 404

Use this when you need the numeric representation.

Converting Integers to Enums

Converting an integer to an enum is more delicate.

The integer may not match a valid enum tag.

For example, this enum only allows three values:

const Color = enum(u8) {
    red = 1,
    green = 2,
    blue = 3,
};

The integer 9 is not a valid Color.

Zig provides tools for this kind of conversion, but you must be careful because not every integer is valid. In beginner code, avoid converting raw integers into enums unless you are parsing external data and validating it carefully.

A safer design is often to parse with a switch:

fn colorFromByte(byte: u8) ?Color {
    return switch (byte) {
        1 => .red,
        2 => .green,
        3 => .blue,
        else => null,
    };
}

Now the function returns ?Color.

That means: either a valid Color, or null.

Enum Literals

A value like this is an enum literal:

.red

It does not name the enum type directly.

So Zig needs context.

This works:

const color: Color = .red;

The type annotation provides the context.

This also works:

fn paint(color: Color) void {
    _ = color;
}

paint(.red);

The function parameter tells Zig that .red means Color.red.

But this does not work by itself:

const x = .red; // not enough type information

Zig does not know which enum type .red belongs to.

Enums as State

Enums are excellent for representing state.

const ConnectionState = enum {
    disconnected,
    connecting,
    connected,
    closing,
};

Then code can use a switch:

fn canSend(state: ConnectionState) bool {
    return switch (state) {
        .disconnected => false,
        .connecting => false,
        .connected => true,
        .closing => false,
    };
}

This is clearer than using integers:

0 = disconnected
1 = connecting
2 = connected
3 = closing

The enum names carry the meaning.

Enums as Modes

Enums are also good for mode selection.

const OpenMode = enum {
    read,
    write,
    append,
};

A function can accept the enum:

fn openFile(path: []const u8, mode: OpenMode) void {
    _ = path;

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

The call is clear:

openFile("data.txt", .read);

The function signature tells Zig that .read is an OpenMode.

Common Mistake: Using Strings for Fixed Choices

Beginners often reach for strings:

const mode = "read";

This is flexible, but too flexible.

The compiler cannot stop you from writing:

const mode = "raed";

That typo is just another string.

With an enum:

const mode: OpenMode = .read;

A typo becomes a compile error:

const mode: OpenMode = .raed; // error

Use enums when the choices are known ahead of time.

Common Mistake: Adding else Too Early

This works:

switch (state) {
    .connected => true,
    else => false,
}

But it may hide future mistakes.

Suppose you later add a new state:

const ConnectionState = enum {
    disconnected,
    connecting,
    connected,
    closing,
    reconnecting,
};

The else branch will silently handle .reconnecting.

That may or may not be what you want.

For important logic, prefer explicit cases:

switch (state) {
    .disconnected => false,
    .connecting => false,
    .connected => true,
    .closing => false,
    .reconnecting => false,
}

Now the compiler helps you when the enum changes.

The Main Idea

An enum defines a fixed set of named choices.

const Status = enum {
    pending,
    active,
    failed,
    closed,
};

A value of this type must be one of those choices:

const status: Status = .active;

Use enums for states, modes, categories, commands, directions, result kinds, and any case where a value should come from a known list. Enums make invalid states harder to write and valid states easier to read.