Optionals and errors both describe a value that may not be produced.
They mean different things.
An optional means absence.
?usizeThis says:
there may be a
usize, or there may be no value
An error union means failure.
error{Invalid}!usizeThis says:
there may be a
usize, or the operation may fail withInvalid
Use an optional when no value is a normal result.
Use an error when the operation could not be completed.
Searching a slice is a good optional case:
fn indexOfByte(buf: []const u8, target: u8) ?usize {
for (buf, 0..) |b, i| {
if (b == target) {
return i;
}
}
return null;
}If the byte is not found, nothing went wrong. The answer is simply absent.
Opening a file is an error case:
const std = @import("std");
pub fn main() !void {
const file = try std.fs.cwd().openFile("input.txt", .{});
defer file.close();
_ = file;
}If the file cannot be opened, there is a reason. The path may not exist. Permission may be denied. The operation may exceed an OS limit.
That is not mere absence. It is failure.
The distinction keeps programs honest.
A function returning ?T gives the caller one question:
if (value) |v| {
// use v
} else {
// no value
}A function returning E!T gives the caller another question:
const v = operation() catch |err| {
// handle err
};When both are possible, the types can be combined.
fn findConfig(name: []const u8) !?[]const u8 {
_ = name;
// May fail while reading.
// May also succeed and find nothing.
return null;
}The return type is:
!?[]const u8Read it as:
the operation may fail; if it succeeds, it may return a slice or no slice
Use it like this:
const result = try findConfig("port");
if (result) |value| {
std.debug.print("found: {s}\n", .{value});
} else {
std.debug.print("not found\n", .{});
}The try handles the error part.
The if handles the optional part.
These two checks should not be collapsed into one vague result. Zig gives each case its own type.
A bad design uses null for everything:
fn readFileMaybe(path: []const u8) ?[]u8 {
_ = path;
return null;
}The caller cannot tell whether the file did not exist, permission was denied, memory ran out, or the file was empty.
A better design uses an error union:
fn readFile(path: []const u8) ![]u8 {
_ = path;
return error.NotImplemented;
}Now failure is visible.
Optionals are for missing values.
Errors are for failed operations.
Exercise 9-14. Decide whether each function should return ?T or !T: search in an array, open a file, parse an integer, find a field in a struct-like table.
Exercise 9-15. Write a function findFirstLongWord that returns ?[]const u8.
Exercise 9-16. Write a function type for loading a config value from disk where reading may fail and the key may be absent.