Understanding Error Unions in Zig: Safe and Explicit Error Handling

Zig replaces exceptions with something better: error unions. They give you fine-grained, type-safe error handling without hiding control flow. In this article, we’ll break down how error unions work, when to use them, and how they make your code safer and clearer. Step 1: What Is an Error Union? An error union is a value that may either be a result or an error. Think of it like Result in Rust, but built into the language. The syntax: fn doThing() !i32 { return 42; // could also return an error } This means doThing might return an i32, or an error. Step 2: Handling Error Unions Zig makes handling errors explicit — no hidden try/catch. You deal with them directly. Using try const result = try doThing(); std.debug.print("Value: {}\n", .{result}); If doThing() succeeds, result holds the value. If it fails, the error is returned up the call stack. Using catch const result = doThing() catch { std.debug.print("Something went wrong\n", .{}); return; }; catch lets you recover from errors in-line. Step 3: Defining Your Own Errors You can define custom error sets to make your APIs more expressive: const MyError = error{ NotFound, PermissionDenied, }; fn readFile(path: []const u8) MyError![]u8 { if (path.len == 0) return MyError.NotFound; return "file contents"; } This makes it clear exactly what errors your function might return — no guessing. Step 4: Propagating vs. Handling Zig encourages you to handle errors when you can, or propagate them clearly using try. fn wrapper() !void { const contents = try readFile("config.toml"); std.debug.print("{s}\n", .{contents}); } This makes it obvious that wrapper may also fail. ✅ Pros and ❌ Cons of Error Unions ✅ Pros:

May 9, 2025 - 02:48
 0
Understanding Error Unions in Zig: Safe and Explicit Error Handling

Zig replaces exceptions with something better: error unions. They give you fine-grained, type-safe error handling without hiding control flow. In this article, we’ll break down how error unions work, when to use them, and how they make your code safer and clearer.

Step 1: What Is an Error Union?

An error union is a value that may either be a result or an error. Think of it like Result in Rust, but built into the language.

The syntax:

fn doThing() !i32 {
    return 42; // could also return an error
}

This means doThing might return an i32, or an error.

Step 2: Handling Error Unions

Zig makes handling errors explicit — no hidden try/catch. You deal with them directly.

Using try

const result = try doThing();
std.debug.print("Value: {}\n", .{result});
  • If doThing() succeeds, result holds the value.
  • If it fails, the error is returned up the call stack.

Using catch

const result = doThing() catch {
    std.debug.print("Something went wrong\n", .{});
    return;
};
  • catch lets you recover from errors in-line.

Step 3: Defining Your Own Errors

You can define custom error sets to make your APIs more expressive:

const MyError = error{
    NotFound,
    PermissionDenied,
};

fn readFile(path: []const u8) MyError![]u8 {
    if (path.len == 0) return MyError.NotFound;
    return "file contents";
}

This makes it clear exactly what errors your function might return — no guessing.

Step 4: Propagating vs. Handling

Zig encourages you to handle errors when you can, or propagate them clearly using try.

fn wrapper() !void {
    const contents = try readFile("config.toml");
    std.debug.print("{s}\n", .{contents});
}

This makes it obvious that wrapper may also fail.

✅ Pros and ❌ Cons of Error Unions

✅ Pros: