Enums

for Zig 0.15.2 Buy

Enums

Enums let us define data types with a limited set of values, each one given a name.


const EnumName = enum {

     value_name,

      …

     value_name_N,

}; 


In the case of a motor, freshly disassembled from a drone and connected to our measuring instruments, we could define two main states: on and off.

enum_e_status.zig

const std = @import("std");

const print = std.debug.print;

const EngineStatus = enum {

    on,

    off,

};

pub fn main() void {

    const e_status_1 = EngineStatus.off;

    print("Motor state 1: {}\n", .{e_status_1});

}

$ zig run enum_e_status.zig

Motor state 1: .off

If we only have these two states, a single bit would be enough to represent them. We can specify the integer type used to store the enum value:

  const EngineStatus = enum(u1) {

    on,

    off,

  };

If we define the integer type, we need to consider its range:

  const EngineStatus = enum(u1) {

    on,

    off,

    unknown, // <- this one no longer fits in a single bit

  };

error: enumeration value '2' too large for type 'u1'

The ordinal values of enums start at 0. We can access them using:

  @intFromEnum(EngineStatus.off) // -> 1

We can also define the values explicitly:

  const EngineStatus = enum(i8) { // i8 to allow negative values

    on = 1,

    off = 0,

    failure = -1,

  };

If we don't explicitly define an ordinal value after having manually assigned others, Zig will try to assign it based on the last defined value:

  const EngineStatus = enum(i8) {

    on = 1,

    off = 0,

    failure = -1,

    testing, // added without manual assignment

  };

This will cause an error because the compiler will try to assign testing the value 0, which is already taken by off:

error: enum tag value 0 already taken

    testing,

    ^~~~~~~

To fix it, we just need to put the definitions in the correct order:

  // manual values in order

  const EngineStatus = enum(i8) {

    failure = -1,

    off = 0,

    on = 1,

    testing,// added without manual assignment

  };

Non-exhaustive enums

Check out this way of building an enum:

  const EngineStatus = enum(i8) { <- tag type is mandatory

    off = 0,

    on = 1,

    failure = -1,

    _, // <- marks it as non-exhaustive

  };

Until now, we've always declared exhaustive enums, where all possible values are defined. But by using _ and an explicit type like i8, we've created a non-exhaustive enum.

enum_non_exhaustive.zig

const std = @import("std");

const print = std.debug.print;

const EngineStatus = enum(u8) { // -> tag type is mandatory

    off,

    on,

    failure,

    _, // <- marks it as non-exhaustive

};

pub fn main() void {

    // normal value with a declared tag

    const e_1: EngineStatus = .on;

    // legal, even 33 isn't a declared tag

    const e_2: EngineStatus = @enumFromInt(33);

    print(

        "status 1 = {}, int = {}\n",

        .{ e_1, @intFromEnum(e_1) },

    );

    print(

        "status 2 = {}, int = {}\n",

        .{ e_2, @intFromEnum(e_2) },

    );

    // cover values with else

    switch (e_2) {

        .off => print("engine off\n", .{}),

        .on => print("engine on\n", .{}),

        // .failure => print("engine failure\n", .{}),

        // if we omit .failure, _ => isn’t enough,

        // we need else

        else => |n_val| print("other value: {}\n", .{n_val}),

        // if you only include only _ you’ll get an error

        // because .failure is declared but not covered

        _ => |n_val| print("undeclared value: {}\n", .{n_val}),

    }

}

$ zig run enum_nonexhaustive.zig

status 1 = .on, int = 1

status 2 = @enumFromInt(33), int = 33

undeclared value: @enumFromInt(33)

Note the possible range of u8: from 0 to 255. Now that the enum is non-exhaustive, Zig can no longer assume we've covered the entire range. If we do a switch on this type, Zig requires us to include either an else (for uncovered values), or  _ (untagged values), or both.

  // cover values with else

  switch (e_status) {

    .off => ...,

    .on => ...,

    // if we omit one, _ => isn’t enough,

    // we need else

    else => |n_val| ... // else is mandatory

    _ => |n_val| ... // only undeclared values

  }

It's also interesting that we can force an undeclared value like this:

  const e_status_x: EngineStatus = @enumFromInt(33);

  // legal, even 33 isn't a declared tag

This is useful if, for example, the value you're converting comes from a file, but it also means you have to be very careful when validating it. At first, non-exhaustive enums might be a bit advanced for a beginner in Zig, so we'll dive into them more in upcoming books in the series.

Variables and constants inside the enum

We can declare variables and constants inside the enum. These belong to the type itself; their scope will be global, and their value won’t be tied to an instance.

enum_2.zig

const std = @import("std");

const print = std.debug.print;

const EngineStatus = enum(u2) {

    // this variable gives a value to the type

    // not to individual instances

    var n_starts: u32 = 0;

    on,

    off,

    failure,

};

pub fn main() void {

    const e_status1 = EngineStatus.on;

    // global access, modifies the type’s data

    EngineStatus.n_starts += 1;

    var e_status2 = EngineStatus.on;

    // global access

    EngineStatus.n_starts += 1;

    print("Motor state 1: {}\n", .{e_status1});

    e_status2 = EngineStatus.off;

    print("Motor state 2: {}\n", .{e_status2});

    print("Motor start counter: {}\n", .{EngineStatus.n_starts});

}

$ zig run enum_2.zig

Motor state 1: .on

Motor state 2: .off

Motor start counter: 2

Enum methods

We can define functions inside an enum. When a function is part of an enum, struct, or union, it's called a method. These don’t depend on a specific instance unless they receive one as a parameter (self).

enum_6engines.zig

const std = @import("std");

const print = std.debug.print;

const EngineStatus = enum(u3) {

    var n_can_start: u32 = 0; // EngineStatus type data

    failure,

    off,

    on,

    autorepairing,

    // method for a specific instance (receives self)

    fn can_start(self: EngineStatus) bool {

        return switch (self) {

            .autorepairing, .failure => false,

            else => true,

        };

    }

    // method of the type (does not receive self)

    fn count_can_start() void {

        EngineStatus.n_can_start += 1;

    }

};

pub fn main() void {

    const a_engines = [_]EngineStatus{

        .off,

        .off,

        .on,

        .autorepairing,

        .off,

        .failure,

    };

    for (a_engines) |e_status| {

        if (e_status.can_start()) EngineStatus.count_can_start();

        print("{}\n", .{e_status});

    }

    print("There are {} engines available.\n", .{EngineStatus.n_can_start});

}

$ zig run enum_6engines.zig

.off

.off

.on

.autorepairing

.off

.failure

There are 4 engines available.

Useful functions for working with enums:

enum_values.zig

const std = @import("std");

const print = std.debug.print;

const EngineStatus = enum(i8) {

    failure = -1,

    off = 0,

    on = 1,

    testing, // 2

    autorepairing, // 3

};

pub fn main() void {

    // std.meta.fields - field information

    print("\nNumber of states: {any}\n", .{std.meta.fields(EngineStatus).len});

    // std.meta.tags - all enum values as an array

    print("All detected possible states:\n", .{});

    for (std.meta.tags(EngineStatus)) |e_status| {

        print(" {} - \"{s}\" = {d}\n",

            // state

            .{

                e_status,

                @tagName(e_status),

                @intFromEnum(e_status),

            });

    }

}

$ zig run enum_values.zig

Number of states: 5

All detected possible states:

.failure - "failure" = -1

.off - "off" = 0

.on - "on" = 1

.testing - "testing" = 2

.autorepairing - "autorepairing" = 3

Enums, Structs, Errors, Unions
Structs
© 2025 - 2026 Zen of Zig