Overview
Zig treats errors as explicit values rather than exceptions. Error sets and error unions let functions return either a normal value or an error value; handling is explicit at call sites. This design keeps control flow clear and avoids hidden exception paths.
Comparison
Quick reference comparing the three main error-handling constructs.
| Construct | Purpose | Behavior | When to use |
|---|---|---|---|
| try | Propagate errors upward | Returns the error from the current function if the expression yields an error | When you want simple propagation without handling |
| catch | Handle or transform an error locally | Evaluates an expression when an error occurs; can map to a value or return a different error | When you need to recover, map, or log specific errors |
| errdefer | Run cleanup only on error return | Executes its block if the current scope is exiting due to an error | When partial side effects must be undone only on failure |
Examples
Using try for propagation
Use try when you want to forward any error from a callee to the caller without additional handling.
// returns ![]u8 or an error
fn readFile(path: []const u8) ![]u8 {
const file = try std.fs.cwd().openFile(path, .{});
defer file.close();
return try file.readToEndAlloc(std.heap.page_allocator, 4096);
}
Here, any error from openFile or readToEndAlloc is returned to the caller.
Using catch to recover or map
catch follows an expression and runs when that expression yields an error. It can return a fallback value or transform the error.
const result = someOperation() catch |err| {
// handle specific error or return a default
if (err == .NotFound) return defaultValue;
// rethrow a different error
return error.CustomFailure;
};
Using errdefer for error-only cleanup
errdefer registers a block that runs only if the current scope exits with an error. This is ideal for undoing partial effects when a function fails.
fn writeWithTemp(path: []const u8, data: []const u8) !void {
var created = false;
const temp = try createTempFile(path);
errdefer if (created) std.fs.cwd().removeFile(temp) catch {};
// if an error occurs after this point, the temp file will be removed
try writeToFile(temp, data);
created = true;
try std.fs.cwd().rename(temp, path);
}
Patterns and Best Practices
Because errors are values, prefer explicit handling at the level that can make a meaningful decision.
- Propagate with try when the caller is better suited to decide how to handle the error.
- Use catch to convert low-level errors into higher-level domain errors or to provide sensible defaults.
- Use errdefer for cleanup that should only run on failure; pair it with normal
deferfor always-run cleanup. - Keep error sets small and descriptive so callers can match and handle specific cases easily.
- Test error paths — error-only cleanup is a common source of bugs and leaks if not exercised in tests.
Testing and Debugging Error Paths
Error paths are often under-tested. Use fault-injection or test helpers to trigger errors and verify that errdefer cleanup runs and that resources are not leaked. Libraries exist to help inject failures during tests so error-handling code is exercised reliably.
Checklist for robust error handling
- Write unit tests that simulate each error case.
- Verify that
errdeferblocks run only on error returns. - Ensure
deferanderrdeferordering is correct for nested resources. - Keep error messages and error set names descriptive for easier debugging.