🎓 A1Learner
🔍
0 of 46 lessons completed

Zig Error Sets in Depth

An error set is a named group of error values a function can return. This lesson goes deeper into naming, combining, and widening error sets.

Naming errors

The name of an error value is text, and error values compare equal only to themselves.

const std = @import("std");
const FileError = error{ NotFound, BadInput, Locked };
pub fn main() !void {
    const out = std.io.getStdOut().writer();
    try out.print("{s}\n", .{@errorName(FileError.NotFound)});
    try out.print("{s}\n", .{@errorName(FileError.Locked)});
    try out.print("{}\n", .{FileError.NotFound == FileError.NotFound});
    try out.print("{}\n", .{FileError.NotFound == FileError.BadInput});
}

Combining two sets

Two error sets combine with ||, and a function can return errors from either source.

const std = @import("std");
const ReadError = error{ NotFound, BadInput };
const NetError = error{ Timeout, BadInput };
const AnySourceError = ReadError || NetError;
fn readFile(ok: bool) ReadError!i32 {
    if (ok) return 42;
    return ReadError.NotFound;
}
fn fetch(ok: bool) NetError!i32 {
    if (ok) return 7;
    return NetError.Timeout;
}
fn either(which: u8) AnySourceError!i32 {
    if (which == 0) return try readFile(false);
    if (which == 1) return try fetch(false);
    return 99;
}
pub fn main() !void {
    const out = std.io.getStdOut().writer();
    if (either(0)) |v| {
        try out.print("value: {d}\n", .{v});
    } else |err| {
        try out.print("error: {s}\n", .{@errorName(err)});
    }
    if (either(1)) |v| {
        try out.print("value: {d}\n", .{v});
    } else |err| {
        try out.print("error: {s}\n", .{@errorName(err)});
    }
    if (either(2)) |v| {
        try out.print("value: {d}\n", .{v});
    } else |err| {
        try out.print("error: {s}\n", .{@errorName(err)});
    }
}

Widening a small set

A smaller error set fits inside a larger set that contains all its members.

const std = @import("std");
const Small = error{ NotFound };
const Large = error{ NotFound, BadInput, Locked };
pub fn main() !void {
    const out = std.io.getStdOut().writer();
    const small: Small = Small.NotFound;
    const large: Large = small;
    try out.print("{s}\n", .{@errorName(large)});
    const also: Large = Large.Locked;
    try out.print("{s}\n", .{@errorName(also)});
    if (large == Large.NotFound) {
        try out.print("matches small set member\n", .{});
    }
}

Example

const std = @import("std");
const ParseError = error{ Empty, BadDigit };
const LoadError = error{ NotFound, Empty };
const LoadParseError = ParseError || LoadError;
fn load(ok: bool) LoadError!i32 {
    if (ok) return 10;
    return LoadError.NotFound;
}
fn parse(v: i32) ParseError!i32 {
    if (v == 0) return ParseError.Empty;
    return v * 3;
}
fn loadAndParse(ok: bool) LoadParseError!i32 {
    const v = try load(ok);
    return try parse(v);
}
pub fn main() !void {
    const out = std.io.getStdOut().writer();
    if (loadAndParse(true)) |v| {
        try out.print("result: {d}\n", .{v});
    } else |err| {
        try out.print("failed: {s}\n", .{@errorName(err)});
    }
    if (loadAndParse(false)) |v| {
        try out.print("result: {d}\n", .{v});
    } else |err| {
        try out.print("failed: {s}\n", .{@errorName(err)});
    }
    const small: LoadError = LoadError.Empty;
    const wide: LoadParseError = small;
    try out.print("widened: {s}\n", .{@errorName(wide)});
}

Key points

  • Error set: an error set is a named list of error values a function can return.
  • Error name: an error name is the text form of an error value, given by @errorName without any prefix.
  • Error values compare with ==, so a handler can test for one exact error.
  • Merged set: a merged set is the combination of two error sets, written with ||, and a repeated error appears only once.
  • A value from a smaller error set can be stored where a larger error set is expected, when every member of the smaller set is also in the larger set.
  • Inferred set: an inferred set is an error set the compiler works out from the errors a function actually returns.

loadAndParse combines a loader and a parser. Each helper returns its own error set, and the combined function returns the merged set. The caller handles both cases with one capture, and a value from the smaller set widens into the merged set.

Try it Yourself

Edit the code, then press Run.

Running is simulated

Exercise

What text does @errorName give for the value FileError.NotFound?