Zig Errors or Optionals
An optional and an error union both describe a result that might be missing. An optional marks the gap with null, and an error union marks it with a named error.
A search can answer with an optional
findIndex returns the index or null. The found index prints, and orelse turns the missing one into -1.
const std = @import("std");
fn findIndex(nums: []const i32, target: i32) ?i32 {
for (nums, 0..) |value, i| {
if (value == target) {
return @intCast(i);
}
}
return null;
}
pub fn main() !void {
const out = std.io.getStdOut().writer();
const nums = [_]i32{ 4, 8, 15 };
const found = findIndex(&nums, 8) orelse -1;
const missing = findIndex(&nums, 99) orelse -1;
try out.print("{d}\n", .{found});
try out.print("{d}\n", .{missing});
}The same search can answer with an error
findIndexErr returns the index or error.NotFound. The found index prints, and catch turns the error into -1.
const std = @import("std");
const FindError = error{NotFound};
fn findIndexErr(nums: []const i32, target: i32) FindError!i32 {
for (nums, 0..) |value, i| {
if (value == target) {
return @intCast(i);
}
}
return error.NotFound;
}
pub fn main() !void {
const out = std.io.getStdOut().writer();
const nums = [_]i32{ 4, 8, 15 };
const found = try findIndexErr(&nums, 15);
const missing = findIndexErr(&nums, 99) catch -1;
try out.print("{d}\n", .{found});
try out.print("{d}\n", .{missing});
}A miss looks different in each shape
The optional miss prints as null. The error miss becomes -1 through catch.
const std = @import("std");
const FindError = error{NotFound};
fn findIndex(nums: []const i32, target: i32) ?i32 {
for (nums, 0..) |value, i| {
if (value == target) {
return @intCast(i);
}
}
return null;
}
fn findIndexErr(nums: []const i32, target: i32) FindError!i32 {
for (nums, 0..) |value, i| {
if (value == target) {
return @intCast(i);
}
}
return error.NotFound;
}
pub fn main() !void {
const out = std.io.getStdOut().writer();
const nums = [_]i32{ 4, 8, 15 };
try out.print("{?}\n", .{findIndex(&nums, 99)});
const missing = findIndexErr(&nums, 99) catch -1;
try out.print("{d}\n", .{missing});
}Example
const std = @import("std");
const FindError = error{NotFound};
fn findIndex(nums: []const i32, target: i32) ?i32 {
for (nums, 0..) |value, i| {
if (value == target) {
return @intCast(i);
}
}
return null;
}
fn findIndexErr(nums: []const i32, target: i32) FindError!i32 {
for (nums, 0..) |value, i| {
if (value == target) {
return @intCast(i);
}
}
return error.NotFound;
}
pub fn main() !void {
const out = std.io.getStdOut().writer();
const nums = [_]i32{ 4, 8, 15 };
const opt_found = findIndex(&nums, 8) orelse -1;
const err_found = try findIndexErr(&nums, 8);
const opt_miss = findIndex(&nums, 99) orelse -1;
const err_miss = findIndexErr(&nums, 99) catch -1;
try out.print("{d}\n", .{opt_found});
try out.print("{d}\n", .{err_found});
try out.print("{d}\n", .{opt_miss});
try out.print("{d}\n", .{err_miss});
}Key points
- Optional result: a ?i32 return holds a value or null when nothing was found.
- Error result: an error union return holds a value or a named error when nothing was found.
- Normal gap: use an optional when a missing result is an ordinary outcome.
- Reason needed: use an error union when the caller needs to know why the work failed.
- Handling: orelse or an if capture reads an optional, while catch reads an error union.
In this example, both searches look for 8 in the same array and both report index 1, one through orelse and one through try. Both then look for 99 and miss. Each shape turns its own miss into -1, the optional through orelse and the error union through catch.
Note
both shapes force the caller to deal with the missing case before using the value.
Try it Yourself
Edit the code, then press Run.
Running is simulated
Exercise
A lookup for a word in a list often finds nothing, and that miss is a normal outcome. Which return shape fits best?