An error set is a group of possible error names.
In Zig, errors are not strings. They are not exception objects. They are named values.
For example:
const FileError = error{
NotFound,
PermissionDenied,
InvalidFormat,
};This defines an error set named FileError.
It contains three possible errors:
error.NotFound
error.PermissionDenied
error.InvalidFormatEach name describes one kind of failure.
Error Names
An error name starts with error. when used as a value.
return error.NotFound;This means: return the error named NotFound.
The error name itself is global. If two error sets both contain NotFound, they refer to the same error name.
const FileError = error{
NotFound,
};
const NetworkError = error{
NotFound,
Timeout,
};Both sets include error.NotFound.
But the sets are different. FileError contains only NotFound. NetworkError contains NotFound and Timeout.
Using an Error Set in a Function
You can write a function that returns one of the errors from a specific set:
const ParseError = error{
EmptyInput,
InvalidDigit,
};
fn parseDigit(c: u8) ParseError!u8 {
if (c < '0' or c > '9') {
return error.InvalidDigit;
}
return c - '0';
}The return type is:
ParseError!u8Read it as:
this function returns either a ParseError or a u8So the function can return:
success: a u8 value
failure: error.EmptyInput or error.InvalidDigitEven though this particular function only returns InvalidDigit, the type says it may return anything in ParseError.
Inferred Error Sets
Zig can often infer the error set for you.
Instead of writing:
fn parseDigit(c: u8) ParseError!u8 {
if (c < '0' or c > '9') {
return error.InvalidDigit;
}
return c - '0';
}you can write:
fn parseDigit(c: u8) !u8 {
if (c < '0' or c > '9') {
return error.InvalidDigit;
}
return c - '0';
}The !u8 form means Zig infers the error set from the function body.
For small examples, inferred error sets are convenient.
For public APIs, explicit error sets are often clearer, because the function signature tells readers exactly which errors belong to the contract.
Any Error
Zig also has the global error set:
anyerrorThis means any error name.
For example:
fn run() anyerror!void {
return error.SomethingWentWrong;
}This works, but anyerror is broad. It tells the caller that almost anything can fail, but not what specifically.
For application code, anyerror can be acceptable.
For library code, a smaller explicit error set is usually better. It gives callers a more precise contract.
Combining Error Sets
Error sets can be merged.
const ReadError = error{
NotFound,
PermissionDenied,
};
const ParseError = error{
InvalidFormat,
};
const ConfigError = ReadError || ParseError;Now ConfigError contains:
error.NotFound
error.PermissionDenied
error.InvalidFormatThis is useful when one operation has several phases.
For example, reading a config file can fail while reading the file or while parsing its contents.
fn loadConfig() ConfigError!void {
// read file
// parse file
}The type says exactly what kind of failures are part of this operation.
Error Sets as Documentation
Error sets are useful because they document failure.
Compare this:
fn loadConfig() !Config {with this:
fn loadConfig() ConfigError!Config {The second version gives more information.
A reader can inspect ConfigError and see the possible failure cases.
const ConfigError = error{
FileNotFound,
PermissionDenied,
InvalidSyntax,
MissingRequiredField,
};This is not just documentation in comments. The compiler also understands it.
Handling Specific Errors
Because errors are named values, you can handle them with switch.
const result = parseDigit('x') catch |err| switch (err) {
error.InvalidDigit => {
std.debug.print("not a digit\n", .{});
return;
},
error.EmptyInput => {
std.debug.print("empty input\n", .{});
return;
},
};This says: if parsing fails, inspect the error and choose what to do.
For small cases, this may feel verbose. In real programs, it becomes useful because each error can get a different response.
A missing file might create a default file.
Permission failure might show a message.
Invalid input might ask the user to try again.
Out of memory might stop the program.
Different failures need different behavior.
Error Set Coercion
A smaller error set can usually fit into a larger one.
const SmallError = error{
NotFound,
};
const BigError = error{
NotFound,
PermissionDenied,
};
fn small() SmallError!void {
return error.NotFound;
}
fn big() BigError!void {
try small();
}This works because every possible SmallError is also inside BigError.
The function big promises it may return NotFound or PermissionDenied. Calling small can only produce NotFound, so the error can be propagated safely.
But the reverse direction would not be safe.
A function that can return PermissionDenied cannot be treated as one that only returns NotFound.
Error Sets Keep APIs Honest
Suppose you write this:
const LoginError = error{
InvalidPassword,
UserLocked,
};
fn login() LoginError!void {
return error.DatabaseDown;
}This is invalid because DatabaseDown is not part of LoginError.
The compiler prevents the function from returning an error outside its declared contract.
To fix it, you either add the error:
const LoginError = error{
InvalidPassword,
UserLocked,
DatabaseDown,
};or handle it inside the function instead of returning it.
This is valuable. The error set keeps the function honest.
Choosing Good Error Names
Good error names should describe the failure clearly.
Weak names:
error.Bad
error.Failed
error.Unknown
error.ErrorBetter names:
error.InvalidHeader
error.MissingField
error.PermissionDenied
error.ConnectionClosed
error.UnsupportedVersionThe goal is not to write long names. The goal is to make the failure understandable at the call site.
This line should tell the reader something useful:
return error.UnsupportedVersion;When to Use Small Error Sets
Use small explicit error sets when you are designing a stable API.
For example:
const TokenizeError = error{
InvalidCharacter,
UnterminatedString,
InvalidEscape,
};This tells callers exactly what can happen during tokenization.
Small sets are good for:
library APIs
parsers
protocol code
file format readers
database layers
public modulesThey make your code easier to use because callers can handle every failure intentionally.
When Inferred Error Sets Are Fine
Inferred error sets are useful when the exact set is not part of the external contract.
For example:
fn helper() !void {
try doStepOne();
try doStepTwo();
try doStepThree();
}For private helper functions, this is often fine.
The function is internal. The caller is nearby. The exact error set may change as the implementation changes.
For public functions, be more careful. If other code depends on your function, the error set is part of the API.
The Core Idea
An error set is a precise list of failures.
It lets Zig say:
this function may fail,
and these are the possible reasonsThat is more useful than a hidden exception, and more precise than a generic failure code.
Error sets are one of the main reasons Zig error handling stays explicit, typed, and readable.