Lesson 6 of 15

Error Handling

Errors as Values in Zig

Zig does not have exceptions. Like Go, it treats errors as values. But Zig goes further: errors are a first-class part of the type system, enforced at compile time.

Error Sets

An error set is an enumeration of possible error values:

const FileError = error{
    NotFound,
    PermissionDenied,
    OutOfMemory,
};

You can also let the compiler infer the error set from a function's body by using ! without naming a specific set.

Error Union Types

An error union combines a normal type with an error set. The syntax is ErrorSet!ValueType:

fn divide(a: f64, b: f64) error{DivisionByZero}!f64 {
    if (b == 0.0) return error.DivisionByZero;
    return a / b;
}

The inferred shorthand uses just !:

fn divide(a: f64, b: f64) !f64 {
    if (b == 0.0) return error.DivisionByZero;
    return a / b;
}

try

The try keyword unwraps an error union. If the value is an error, it immediately returns that error from the current function:

fn doWork() !void {
    const result = try divide(10.0, 0.0);
    // If divide returns an error, doWork returns that same error
    std.debug.print("result: {}\n", .{result});
}

try x is shorthand for x catch |err| return err. It propagates errors up the call stack without boilerplate.

Red alert! Error propagation in Zig works much like the Enterprise's alert system --- the problem is detected at the source and escalated up the chain of command until someone handles it.

catch

The catch keyword handles errors inline. You can provide a default value or run a block:

const value = divide(10.0, 0.0) catch 0.0;
// value is 0.0 if divide returned an error

With a capture:

const value = divide(10.0, 0.0) catch |err| {
    std.debug.print("error: {}\n", .{err});
    return;
};

Error in main

The main function can return !void to propagate errors. If an error reaches main, the program terminates with an error trace:

pub fn main() !void {
    const result = try divide(10.0, 3.0);
    std.debug.print("{}\n", .{result});
}

Custom Error Sets

You can define descriptive error sets for your domain:

const ValidationError = error{
    TooShort,
    TooLong,
    InvalidCharacter,
};

fn validateUsername(name: []const u8) ValidationError!void {
    if (name.len < 3) return error.TooShort;
    if (name.len > 20) return error.TooLong;
}

Merging Error Sets

Error sets can be merged with ||:

const IoError = error{ ReadFailed, WriteFailed };
const ParseError = error{ InvalidFormat, UnexpectedToken };
const AppError = IoError || ParseError;

Your Task

Write a function validateAge that takes an i32 and returns error{Negative,Unrealistic}!void:

  • If age is negative, return error.Negative
  • If age is greater than 150, return error.Unrealistic
  • Otherwise, return normally (no error)

Then write a helper function checkAge that takes an i32, calls validateAge, and prints:

  • "valid" if no error
  • "age cannot be negative" if the error is Negative
  • "age is unrealistic" if the error is Unrealistic

Each message should be followed by a newline.

Zig runtime loading...
Loading...
Click "Run" to execute your code.