Zig Error Union Patterns
An error union is a value that is either a result or an error. This lesson shows the patterns for passing, catching, and branching on that value.
Chaining with try
Try chains helpers: an error in a helper returns from the caller at once.
const std = @import("std");
const LookupError = error{ NotFound, Empty };
fn lookup(ok: bool) LookupError!i32 {
if (ok) return 42;
return LookupError.NotFound;
}
fn doubled(ok: bool) LookupError!i32 {
const v = try lookup(ok);
return v * 2;
}
pub fn main() !void {
const out = std.io.getStdOut().writer();
try out.print("doubled: {d}\n", .{try doubled(true)});
try out.print("fallback: {d}\n", .{lookup(false) catch 0});
}
Recovering in catch
A named catch reads the error, then the block gives a recovery value.
const std = @import("std");
const LookupError = error{ NotFound, Empty };
fn lookup(ok: bool) LookupError!i32 {
if (ok) return 42;
return LookupError.NotFound;
}
pub fn main() !void {
const out = std.io.getStdOut().writer();
const a = lookup(false) catch |err| blk: {
try out.print("saw: {s}\n", .{@errorName(err)});
break :blk -1;
};
try out.print("recovered: {d}\n", .{a});
const b = lookup(true) catch |err| blk: {
try out.print("saw: {s}\n", .{@errorName(err)});
break :blk -1;
};
try out.print("kept: {d}\n", .{b});
}
Branching on the result
Payload capture splits the result case from the error case in one if.
const std = @import("std");
const LookupError = error{ NotFound, Empty };
fn lookup(which: u8) LookupError!i32 {
if (which == 0) return 42;
if (which == 1) return LookupError.Empty;
return LookupError.NotFound;
}
fn describe(which: u8) []const u8 {
if (lookup(which)) |v| {
if (v == 42) return "found 42";
return "found other";
} else |err| {
if (err == LookupError.NotFound) return "missing";
return "empty input";
}
}
pub fn main() !void {
const out = std.io.getStdOut().writer();
try out.print("{s}\n", .{describe(0)});
try out.print("{s}\n", .{describe(1)});
try out.print("{s}\n", .{describe(2)});
}
Example
const std = @import("std");
const ScoreError = error{ NoScore, BadScore };
fn score(ok: bool) ScoreError!i32 {
if (ok) return 30;
return ScoreError.NoScore;
}
fn bonus(ok: bool) ScoreError!i32 {
const v = try score(ok);
if (v < 0) return ScoreError.BadScore;
return v + 10;
}
pub fn main() !void {
const out = std.io.getStdOut().writer();
if (bonus(true)) |v| {
try out.print("total: {d}\n", .{v});
} else |err| {
try out.print("failed: {s}\n", .{@errorName(err)});
}
const fallback = bonus(false) catch 0;
try out.print("fallback total: {d}\n", .{fallback});
if (bonus(false)) |v| {
try out.print("total: {d}\n", .{v});
} else |err| {
if (err == ScoreError.NoScore) {
try out.print("no score recorded\n", .{});
} else {
try out.print("bad score\n", .{});
}
}
}
Key points
- Error union: an error union is a value that is either a result or an error.
- Try passes an error straight to the caller, so a helper can chain calls without handling each one.
- Catch with a plain value gives a fallback result when the call fails.
- Catch with a named error lets the handler read the error and choose a recovery value.
- Payload capture: a payload capture is the if with a value capture and an else with an error capture, splitting the two cases.
- A catch block must still produce a value of the result type, even when its body only prints and recovers.
bonus builds on score with try, so a missing score returns at once. The caller shows three handling styles: capture for the success case, a fallback value, and a branch on the exact error. Each style fits a different need, and none of them hides which error happened.
Try it Yourself
Edit the code, then press Run.
Running is simulated
Exercise
What does lookup(false) catch 0 give when lookup returns NotFound?