An error set in Zig is an explicit set of errors that a function may return. If you come from Java, Python, or C#, you might be used to exceptions; Zig prefers to treat errors as visible values.
If you come from C or Go, you’ll already know about error codes or (value, error) tuples. They are explicit, but can also introduce repetitive checks.
Zig takes a different path: errors are values, with direct support in the type system and syntax.
In Zig, an error is neither a class nor a string. Each name is represented by an integer code; by default the global set uses u16, although the error limit can configure a different width. The language prevents accidentally ignoring an error union.
What is an error set
An error set is very similar to an enum. It is a list of possible error names that we define ourselves or that the libraries we use define.
// Define a set of possible errors for a file operation
const FileError = error{
AccessDenied,
FileNotFound,
DiskFull,
BrokenPipe,
};The interesting part is that Error Sets are flexible. The same error name (AccessDenied) has the same unique numeric value throughout the entire program, regardless of where it is defined.
Returning errors
For a function to be able to fail, it must return an error from this set. We use the return keyword with the error.Name prefix.
fn openFile(name: []const u8) FileError {
if (name.len == 0) {
return error.FileNotFound; // Return the error
}
// ... logic ...
return error.AccessDenied;
}Wait… what if the function succeeds and does not fail? How do we return the “good” value? That’s what the ! type is for.
The error union type (!T)
In Zig, a function cannot return “Sometimes an Error and sometimes an Integer”. The type system is strict.
To solve this, Zig introduces the Error Union Type, denoted by the ! operator.
The syntax E!T means: “This function returns an error from set E, OR a value of type T”.
// This function can return either a FileError or a u32
fn readNumber(fail: bool) FileError!u32 {
if (fail) {
return error.DiskFull; // Return the left part (!)
}
return 100; // Return the right part (u32)
}This is conceptually similar to Rust’s Result<T, E> or functional programming’s Either, but integrated so deeply into the syntax that it’s invisible.
Error set inference with !
Often, writing the complete list of errors a function can return is tedious, especially if our function calls ten other functions, each with its own errors.
Zig allows us to use Inferred Error Sets. We simply put ! before the return type, without specifying the name of the error set.
// Zig automatically figures out which errors can come out of here
fn complexFunction() !u32 {
if (condition) return error.SomeError;
if (elseCondition) return error.AnotherError;
return 42;
}The compiler examines the function and creates the error set for us.
- Advantage: if you add an error, you don’t have to manually edit the set in the signature.
- Usage: it’s convenient in application code and private functions. In a public API, an explicit set (
FileError!u32) might be preferable to document what can fail.
Merging error sets
Zig can combine several error sets automatically.
If you have one function that can return error{A} and another that returns error{B}, and you create a function that calls both, the resulting error type is the union error{A, B}.
const NetworkError = error{ Disconnected };
const DBError = error{ TableDoesNotExist };
fn mixedTask() !void {
// Can return NetworkError
try connect();
// Can return DBError
try query();
}
// The actual return type of mixedTask is error{Disconnected, TableDoesNotExist}!voidanyerror: the global type
There is a special type called anyerror. It is the set of all possible errors in the entire program.
fn genericFunction() anyerror!void { ... }Generally, we should avoid using anyerror unless it is strictly necessary (e.g., in pointers to generic functions or interfaces), as we lose specific information about what can fail. Inference (!) is almost always better.