Skip to content

Zig Error Handling

Zig's error handling system is one of its most important features, providing a safe, explicit, and efficient way to handle errors.

Basic Error Concepts

What are Errors?

In Zig, errors are a special value type used to represent exceptional conditions:

zig
const std = @import("std");

// Define error set
const FileError = error{
    NotFound,
    PermissionDenied,
    OutOfMemory,
};

pub fn main() void {
    std.debug.print("Error handling example\n", .{});
}

Error Union Types

Error union types use the ! syntax to indicate a function may return an error or a normal value:

zig
const std = @import("std");

const MathError = error{
    DivisionByZero,
    Overflow,
};

fn divide(a: i32, b: i32) MathError!i32 {
    if (b == 0) return MathError.DivisionByZero;
    return @divTrunc(a, b);
}

pub fn main() void {
    const result1 = divide(10, 2);
    const result2 = divide(10, 0);
    
    std.debug.print("10 / 2 result type: {}\n", .{@TypeOf(result1)});
    std.debug.print("10 / 0 result type: {}\n", .{@TypeOf(result2)});
}

Error Handling Methods

Using if to Handle Errors

The most basic error handling method is using if statements:

zig
const std = @import("std");

const ParseError = error{
    InvalidFormat,
    OutOfRange,
};

fn parseNumber(input: []const u8) ParseError!i32 {
    if (std.mem.eql(u8, input, "42")) {
        return 42;
    } else if (std.mem.eql(u8, input, "100")) {
        return 100;
    } else if (std.mem.eql(u8, input, "999")) {
        return ParseError.OutOfRange;
    } else {
        return ParseError.InvalidFormat;
    }
}

pub fn main() void {
    const inputs = [_][]const u8{ "42", "100", "999", "abc" };
    
    for (inputs) |input| {
        if (parseNumber(input)) |number| {
            std.debug.print("Parse success: '{s}' -> {}\n", .{ input, number });
        } else |err| {
            std.debug.print("Parse failed: '{s}' -> {}\n", .{ input, err });
        }
    }
}

Using catch to Handle Errors

The catch operator can provide default values or execute error handling logic:

zig
const std = @import("std");

const NumberError = error{
    InvalidInput,
    TooLarge,
};

fn validateNumber(num: i32) NumberError!i32 {
    if (num < 0) return NumberError.InvalidInput;
    if (num > 100) return NumberError.TooLarge;
    return num;
}

pub fn main() void {
    const numbers = [_]i32{ 50, -10, 150, 25 };
    
    for (numbers) |num| {
        // Use catch to provide default value
        const safe_num = validateNumber(num) catch 0;
        std.debug.print("Number {} -> Safe value {}\n", .{ num, safe_num });
        
        // Use catch to execute error handling
        const result = validateNumber(num) catch |err| blk: {
            std.debug.print("Validation failed: {} (error: {})\n", .{ num, err });
            break :blk -1;
        };
        std.debug.print("Processing result: {}\n", .{result});
    }
}

Using try to Propagate Errors

The try operator is used to propagate errors to the caller:

zig
const std = @import("std");

const ValidationError = error{
    TooSmall,
    TooLarge,
    InvalidRange,
};

fn validateRange(min: i32, max: i32) ValidationError!void {
    if (min < 0) return ValidationError.TooSmall;
    if (max > 1000) return ValidationError.TooLarge;
    if (min >= max) return ValidationError.InvalidRange;
}

fn processRange(min: i32, max: i32) ValidationError!i32 {
    // Use try to propagate errors
    try validateRange(min, max);
    
    // If no error, continue processing
    return (min + max) / 2;
}

pub fn main() void {
    const ranges = [_][2]i32{
        [_]i32{ 10, 20 },
        [_]i32{ -5, 15 },
        [_]i32{ 10, 2000 },
        [_]i32{ 50, 30 },
    };
    
    for (ranges) |range| {
        const min = range[0];
        const max = range[1];
        
        if (processRange(min, max)) |result| {
            std.debug.print("Range [{}, {}] midpoint: {}\n", .{ min, max, result });
        } else |err| {
            std.debug.print("Range [{}, {}] invalid: {}\n", .{ min, max, err });
        }
    }
}

Error Sets

Defining Error Sets

zig
const std = @import("std");

// File operation errors
const FileError = error{
    NotFound,
    PermissionDenied,
    AlreadyExists,
    DiskFull,
};

// Network errors
const NetworkError = error{
    ConnectionFailed,
    Timeout,
    InvalidAddress,
};

// Combined error set
const IOError = FileError || NetworkError;

fn simulateFileOperation(filename: []const u8) FileError!void {
    if (std.mem.eql(u8, filename, "readonly.txt")) {
        return FileError.PermissionDenied;
    }
    if (std.mem.eql(u8, filename, "missing.txt")) {
        return FileError.NotFound;
    }
    // Simulate success
    std.debug.print("File operation successful: {s}\n", .{filename});
}

pub fn main() void {
    // Test file operations
    const files = [_][]const u8{ "normal.txt", "readonly.txt", "missing.txt" };
    for (files) |file| {
        simulateFileOperation(file) catch |err| {
            std.debug.print("File operation failed: {s} -> {}\n", .{ file, err });
        };
    }
}

Practical Application Examples

File Reading Example

zig
const std = @import("std");

const FileReadError = error{
    FileNotFound,
    PermissionDenied,
    ReadError,
    OutOfMemory,
};

fn readFileContent(allocator: std.mem.Allocator, filename: []const u8) FileReadError![]u8 {
    // Simulate file reading
    if (std.mem.eql(u8, filename, "missing.txt")) {
        return FileReadError.FileNotFound;
    }
    
    if (std.mem.eql(u8, filename, "protected.txt")) {
        return FileReadError.PermissionDenied;
    }
    
    // Simulate successful read
    const content = try allocator.dupe(u8, "File content example");
    return content;
}

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();
    
    const filenames = [_][]const u8{ "normal.txt", "missing.txt", "protected.txt" };
    
    for (filenames) |filename| {
        if (readFileContent(allocator, filename)) |content| {
            defer allocator.free(content);
            std.debug.print("Read file '{s}' success: {s}\n", .{ filename, content });
        } else |err| {
            std.debug.print("Read file '{s}' failed: {}\n", .{ filename, err });
        }
    }
}

Error Handling Best Practices

1. Explicit Error Types

zig
const std = @import("std");

// ✅ Good practice: explicit error types
const DatabaseError = error{
    ConnectionFailed,
    QueryTimeout,
    InvalidQuery,
    RecordNotFound,
};

fn queryDatabase(query: []const u8) DatabaseError![]const u8 {
    if (std.mem.eql(u8, query, "")) {
        return DatabaseError.InvalidQuery;
    }
    
    if (std.mem.eql(u8, query, "SELECT * FROM missing_table")) {
        return DatabaseError.RecordNotFound;
    }
    
    return "Query result";
}

pub fn main() void {
    const queries = [_][]const u8{ "SELECT * FROM users", "", "SELECT * FROM missing_table" };
    
    for (queries) |query| {
        if (queryDatabase(query)) |result| {
            std.debug.print("Query successful: {s}\n", .{result});
        } else |err| {
            // Can handle different error types differently
            switch (err) {
                DatabaseError.ConnectionFailed => std.debug.print("Database connection failed\n", .{}),
                DatabaseError.QueryTimeout => std.debug.print("Query timeout\n", .{}),
                DatabaseError.InvalidQuery => std.debug.print("Invalid query\n", .{}),
                DatabaseError.RecordNotFound => std.debug.print("Record not found\n", .{}),
            }
        }
    }
}

Summary

This chapter covered Zig's error handling system in detail:

  • ✅ Basic error concepts and error union types
  • ✅ Multiple error handling methods: if, catch, try
  • ✅ Error set definition and combination
  • anyerror type usage
  • ✅ Error return tracing
  • ✅ Practical application examples
  • ✅ Error handling best practices

Zig's error handling system forces developers to explicitly handle possible errors, greatly improving program reliability and safety. In the next chapter, we'll learn about Zig's memory management.

Content is for learning and research only.