A Beginner's Guide to Enums and Error Sets in Zig

Enums and error sets in Zig offer powerful ways to model states and failures with clarity and precision. In this tutorial, we’ll cover the basics, then walk through real-world uses of both types — and how they play nicely together. Step 1: Defining Enums Enums let you define a type with named values: const Direction = enum { North, South, East, West, }; You use them like this: const dir = Direction.North; Zig will guarantee at compile time that dir is one of the defined directions. Step 2: Switching on Enums Zig encourages exhaustive switching — the compiler checks for missing cases: switch (dir) { Direction.North => std.debug.print("Going up\n", .{}), Direction.South => std.debug.print("Going down\n", .{}), Direction.East => std.debug.print("Going right\n", .{}), Direction.West => std.debug.print("Going left\n", .{}), } If you forget a case, Zig will complain — and that’s a good thing. Step 3: Using Tagged Unions with Enums You can pair an enum with associated data: const Shape = union(enum) { Circle: f32, Square: f32, }; Now Shape can either be a Circle with a radius, or a Square with a side length: const s = Shape{ .Circle = 5.0 }; You can match and extract values: switch (s) { Shape.Circle => |r| std.debug.print("Circle radius: {}\n", .{r}), Shape.Square => |s| std.debug.print("Square side: {}\n", .{s}), } Step 4: Error Sets Error sets are custom error types. They're first-class citizens in Zig: const MyError = error{ NotFound, PermissionDenied, }; You can use them as return types: fn read() MyError!void { return MyError.NotFound; } And handle them with catch, try, or a switch: const result = read() catch |err| switch (err) { MyError.NotFound => std.debug.print("Missing file\n", .{}), MyError.PermissionDenied => std.debug.print("Access denied\n", .{}), }; ✅ Pros and ❌ Cons of Enums & Error Sets ✅ Pros:

May 10, 2025 - 06:06
 0
A Beginner's Guide to Enums and Error Sets in Zig

Enums and error sets in Zig offer powerful ways to model states and failures with clarity and precision. In this tutorial, we’ll cover the basics, then walk through real-world uses of both types — and how they play nicely together.

Step 1: Defining Enums

Enums let you define a type with named values:

const Direction = enum {
    North,
    South,
    East,
    West,
};

You use them like this:

const dir = Direction.North;

Zig will guarantee at compile time that dir is one of the defined directions.

Step 2: Switching on Enums

Zig encourages exhaustive switching — the compiler checks for missing cases:

switch (dir) {
    Direction.North => std.debug.print("Going up\n", .{}),
    Direction.South => std.debug.print("Going down\n", .{}),
    Direction.East => std.debug.print("Going right\n", .{}),
    Direction.West => std.debug.print("Going left\n", .{}),
}

If you forget a case, Zig will complain — and that’s a good thing.

Step 3: Using Tagged Unions with Enums

You can pair an enum with associated data:

const Shape = union(enum) {
    Circle: f32,
    Square: f32,
};

Now Shape can either be a Circle with a radius, or a Square with a side length:

const s = Shape{ .Circle = 5.0 };

You can match and extract values:

switch (s) {
    Shape.Circle => |r| std.debug.print("Circle radius: {}\n", .{r}),
    Shape.Square => |s| std.debug.print("Square side: {}\n", .{s}),
}

Step 4: Error Sets

Error sets are custom error types. They're first-class citizens in Zig:

const MyError = error{
    NotFound,
    PermissionDenied,
};

You can use them as return types:

fn read() MyError!void {
    return MyError.NotFound;
}

And handle them with catch, try, or a switch:

const result = read() catch |err| switch (err) {
    MyError.NotFound => std.debug.print("Missing file\n", .{}),
    MyError.PermissionDenied => std.debug.print("Access denied\n", .{}),
};

✅ Pros and ❌ Cons of Enums & Error Sets

✅ Pros: