Errors & Error Unions
try and catch.
Error Sets
An error set enumerates the failures a function can produce. Each error is a value with a name and a compile-time identity; sets can be merged with the || operator.
Declaring Sets
const FileError = error{
NotFound,
AccessDenied,
DiskFull,
};
// a function that may fail declares WHICH errors — visible in the type
fn openConfig() FileError![]const u8 {
return error.NotFound;
}
The Global Error Set
anyerror is the set of all errors. It is convenient for prototypes and interfaces, but it hides information; production code prefers narrow, explicit sets. Casting a concrete error into anyerror is automatic; casting back is not.
Error Unions
An error union is written Set!T (or simply !T with an inferred set): either a value of type T or one of the declared errors. It is the return type that makes every failure visible.
Figure 1 — an error union is a tagged value: payload or error, never both.
Returning Errors
fn divide(a: u32, b: u32) !u32 {
if (b == 0) return error.DivisionByZero; // early, visible failure
return a / b;
}
try — Propagate
try unwraps the error union: on success the expression IS the payload; on failure it returns the error from the current function immediately. One character, zero hidden control flow — the jump is explicit in the keyword.
pub fn main() !void {
const q = try divide(10, 2); // 5, or main returns the error
std.debug.print("{d}\n", .{q});
}
catch — Recover
catch handles failure locally. With a value on the right side it provides a fallback; with |err| it captures the error for inspection.
const r = divide(1, 0) catch 0; // 0 on failure — fallback strategy
const s = divide(1, 0) catch |err| blk: {
std.debug.print("failed: {s}\n", .{@errorName(err)});
break :blk 0;
};
errdefer — Cleanup on Error
Paired with errors, errdefer guarantees resources are released exactly when a function unwinds with failure, while defer handles the happy path. Together they replace both goto cleanup and exception destructors.
Basics
fn process() ![]u8 {
const buf = try std.heap.page_allocator.alloc(u8, 128);
errdefer std.heap.page_allocator.free(buf); // only on error
const data = try readAll(); // failure -> errdefer fires
return buf; // success -> caller frees
}
Error Return Traces
In safety-enabled builds, an uncaught error prints an error return trace: the chain of trys that carried the failure, each with file and line. Unlike a crash, this is a first-class diagnostic channel.
Reading a Return Trace
// zig run this file: the runtime reports the exact call chain
pub fn main() !void {
const value = try readConfig(); // reads env, then file
std.debug.print("{s}\n", .{value});
}
// error: ConfigMissing ... readConfig at src/main.zig:3
// load at src/main.zig:8
Common Pitfalls
Overusing anyerror
anyerror is one of the few genuinely unsafe choices: it hides which failures are possible. Start narrow, widen only when a public API truly spans unknown error spaces.
Swallowing Errors
catch with a silent fallback hides real trouble. When you cannot handle an error properly, propagate it with try — a loud failure beats a quiet wrong answer.
Next: Allocators & Memory — the heart of thinking in Zig.