第9章: エラー処理

学習目標

この章を終えると、以下ができるようになります:

  • エラーユニオン型を理解し、使用できる
  • try/catch構文でエラーを処理できる
  • errdeferでリソース解放を自動化できる
  • スタックトレースを活用してデバッグできる
  • Zigのエラー処理の特徴

    エラーは値である

    Zigでは、エラーは例外ではなく通常の値として扱われます。これにより、エラーを明示的に処理することが強制されます。

    const std = @import("std");
    
    // エラーセットの定義
    const FileError = error{
        FileNotFound,
        PermissionDenied,
        DiskFull,
    };
    
    fn openFile(path: []const u8) FileError!void {
        if (path.len == 0) {
            return FileError.FileNotFound;
        }
        // ファイルを開く処理
        std.debug.print("File opened: {s}\n", .{path});
    }
    
    pub fn example() void {
        openFile("test.txt") catch |err| {
            std.debug.print("Error: {}\n", .{err});
        };
    }
    

    エラーユニオン型

    エラーユニオン型は、成功時の値とエラーの両方を表現できる型です。

    const std = @import("std");
    
    fn divide(a: i32, b: i32) !i32 {
        if (b == 0) {
            return error.DivisionByZero;
        }
        return @divTrunc(a, b);
    }
    
    pub fn example() void {
        const result = divide(10, 2) catch |err| {
            std.debug.print("Error: {}\n", .{err});
            return;
        };
        std.debug.print("Result: {}\n", .{result});
    
        const result2 = divide(10, 0) catch |err| {
            std.debug.print("Error: {}\n", .{err});
            return;
        };
        std.debug.print("Result: {}\n", .{result2});
    }
    

    エラーの伝播

    tryキーワードを使うと、エラーを自動的に伝播できます。

    const std = @import("std");
    
    fn readConfig() ![]const u8 {
        return error.FileNotFound;
    }
    
    fn parseConfig(data: []const u8) !i32 {
        if (data.len == 0) {
            return error.InvalidFormat;
        }
        return 42;
    }
    
    fn loadConfig() !i32 {
        // tryは、エラーの場合に自動的にreturnする
        const data = try readConfig();
        const value = try parseConfig(data);
        return value;
    }
    
    pub fn example() void {
        const config = loadConfig() catch |err| {
            std.debug.print("Failed to load config: {}\n", .{err});
            return;
        };
        std.debug.print("Config value: {}\n", .{config});
    }
    

    エラーセットの定義と使用

    基本的なエラーセット

    const std = @import("std");
    
    // 明示的なエラーセット
    const NetworkError = error{
        ConnectionFailed,
        Timeout,
        InvalidResponse,
    };
    
    const DatabaseError = error{
        ConnectionFailed,
        QueryFailed,
        Timeout,
    };
    
    fn connectToServer() NetworkError!void {
        return NetworkError.ConnectionFailed;
    }
    
    fn queryDatabase() DatabaseError!void {
        return DatabaseError.QueryFailed;
    }
    
    pub fn example() void {
        connectToServer() catch |err| {
            std.debug.print("Network error: {}\n", .{err});
        };
    
        queryDatabase() catch |err| {
            std.debug.print("Database error: {}\n", .{err});
        };
    }
    

    エラーセットの結合

    エラーセットは、||演算子で結合できます。

    const std = @import("std");
    
    const FileError = error{
        NotFound,
        PermissionDenied,
    };
    
    const ParseError = error{
        InvalidSyntax,
        UnexpectedToken,
    };
    
    // エラーセットを結合
    const ConfigError = FileError || ParseError;
    
    fn loadAndParseConfig() ConfigError!i32 {
        // FileErrorまたはParseErrorを返せる
        return ParseError.InvalidSyntax;
    }
    
    pub fn example() void {
        const value = loadAndParseConfig() catch |err| {
            std.debug.print("Config error: {}\n", .{err});
            return;
        };
        std.debug.print("Value: {}\n", .{value});
    }
    

    anyerror型

    すべてのエラーを受け入れるanyerror型があります。

    const std = @import("std");
    
    fn riskyOperation() anyerror!i32 {
        // あらゆるエラーを返せる
        return error.SomethingWentWrong;
    }
    
    fn handleAnyError() void {
        const result = riskyOperation() catch |err| {
            std.debug.print("Error occurred: {}\n", .{err});
            return;
        };
        std.debug.print("Success: {}\n", .{result});
    }
    
    pub fn example() void {
        handleAnyError();
    }
    

    try と catch の詳細

    catchでデフォルト値を返す

    const std = @import("std");
    
    fn parseInt(str: []const u8) !i32 {
        if (str.len == 0) {
            return error.EmptyString;
        }
        return 42; // 簡略化のため固定値
    }
    
    pub fn example() void {
        // エラーの場合はデフォルト値を使用
        const value1 = parseInt("123") catch 0;
        const value2 = parseInt("") catch 0;
    
        std.debug.print("value1: {}\n", .{value1});
        std.debug.print("value2: {}\n", .{value2});
    }
    

    catchでエラーを処理

    const std = @import("std");
    
    fn divide(a: i32, b: i32) !f64 {
        if (b == 0) {
            return error.DivisionByZero;
        }
        return @as(f64, @floatFromInt(a)) / @as(f64, @floatFromInt(b));
    }
    
    pub fn example() void {
        const result = divide(10, 0) catch |err| {
            std.debug.print("Error: {}\n", .{err});
            // エラーの種類に応じて処理
            switch (err) {
                error.DivisionByZero => {
                    std.debug.print("Cannot divide by zero!\n", .{});
                },
            }
            return;
        };
        std.debug.print("Result: {d:.2}\n", .{result});
    }
    

    if-elseでエラーを処理

    const std = @import("std");
    
    fn parseNumber(str: []const u8) !i32 {
        if (str.len == 0) {
            return error.EmptyString;
        }
        return 42;
    }
    
    pub fn example() void {
        const str = "";
    
        // if-elseでエラーを処理
        if (parseNumber(str)) |value| {
            std.debug.print("Parsed: {}\n", .{value});
        } else |err| {
            std.debug.print("Parse failed: {}\n", .{err});
        }
    }
    

    errdefer - エラー時の後始末

    基本的なerrdefer

    errdeferは、エラーが発生した場合にのみ実行されるdeferです。

    const std = @import("std");
    
    fn allocateAndProcess(allocator: std.mem.Allocator) ![]u8 {
        const buffer = try allocator.alloc(u8, 1024);
        errdefer allocator.free(buffer); // エラー時のみ解放
    
        // 処理中にエラーが発生する可能性
        if (false) { // 例として条件を変更可能
            return error.ProcessingFailed;
        }
    
        return buffer; // 成功時はbufferを返す
    }
    
    pub fn example() !void {
        const allocator = std.heap.page_allocator;
    
        const buffer = try allocateAndProcess(allocator);
        defer allocator.free(buffer);
    
        std.debug.print("Buffer allocated: {} bytes\n", .{buffer.len});
    }
    

    複数のerrdeferの使用

    const std = @import("std");
    
    const Resource = struct {
        id: i32,
    
        fn create(id: i32) !Resource {
            std.debug.print("Creating resource {}\n", .{id});
            return Resource{ .id = id };
        }
    
        fn destroy(self: Resource) void {
            std.debug.print("Destroying resource {}\n", .{self.id});
        }
    };
    
    fn allocateResources() !void {
        const res1 = try Resource.create(1);
        errdefer res1.destroy();
    
        const res2 = try Resource.create(2);
        errdefer res2.destroy();
    
        const res3 = try Resource.create(3);
        errdefer res3.destroy();
    
        // エラーが発生すると、res3, res2, res1の順に破棄される
        if (true) {
            return error.AllocationFailed;
        }
    
        // 成功時は手動で破棄
        res3.destroy();
        res2.destroy();
        res1.destroy();
    }
    
    pub fn example() void {
        allocateResources() catch |err| {
            std.debug.print("Failed: {}\n", .{err});
        };
    }
    

    errdeferでのエラー処理パターン

    const std = @import("std");
    
    fn openAndReadFile(allocator: std.mem.Allocator, path: []const u8) ![]u8 {
        _ = path;
    
        // ファイルを開く(簡略化のため省略)
        std.debug.print("Opening file...\n", .{});
        errdefer std.debug.print("Failed to open file\n", .{});
    
        // バッファを確保
        const buffer = try allocator.alloc(u8, 1024);
        errdefer allocator.free(buffer);
    
        // ファイルを読み込む(簡略化のため省略)
        std.debug.print("Reading file...\n", .{});
        errdefer std.debug.print("Failed to read file\n", .{});
    
        // エラーをシミュレート
        return error.ReadFailed;
    }
    
    pub fn example() void {
        const allocator = std.heap.page_allocator;
    
        const data = openAndReadFile(allocator, "test.txt") catch |err| {
            std.debug.print("Error: {}\n", .{err});
            return;
        };
        defer allocator.free(data);
    }
    

    スタックトレース

    スタックトレースの取得

    const std = @import("std");
    
    fn level3() !void {
        return error.SomethingWentWrong;
    }
    
    fn level2() !void {
        try level3();
    }
    
    fn level1() !void {
        try level2();
    }
    
    pub fn example() void {
        level1() catch |err| {
            std.debug.print("Error: {}\n", .{err});
            // デバッグビルドではスタックトレースが表示される
        };
    }
    

    エラーリターントレース

    const std = @import("std");
    
    fn deepFunction() !i32 {
        return error.DeepError;
    }
    
    fn middleFunction() !i32 {
        return try deepFunction();
    }
    
    fn topFunction() !i32 {
        return try middleFunction();
    }
    
    pub fn example() void {
        const result = topFunction() catch |err| {
            std.debug.print("Caught error: {}\n", .{err});
            // エラーが伝播した経路が記録される
            return;
        };
        std.debug.print("Result: {}\n", .{result});
    }
    

    カスタムエラーメッセージ

    const std = @import("std");
    
    const CustomError = error{
        InvalidInput,
        OutOfBounds,
        NullPointer,
    };
    
    fn validateInput(value: i32) CustomError!void {
        if (value < 0) {
            std.debug.print("Error context: value={}\n", .{value});
            return CustomError.InvalidInput;
        }
        if (value > 100) {
            std.debug.print("Error context: value={}\n", .{value});
            return CustomError.OutOfBounds;
        }
    }
    
    pub fn example() void {
        validateInput(-5) catch |err| {
            std.debug.print("Validation failed: {}\n", .{err});
        };
    
        validateInput(150) catch |err| {
            std.debug.print("Validation failed: {}\n", .{err});
        };
    }
    

    実践的なエラー処理パターン

    リソース管理パターン

    const std = @import("std");
    
    const File = struct {
        name: []const u8,
        is_open: bool,
    
        fn open(name: []const u8) !File {
            std.debug.print("Opening {s}\n", .{name});
            return File{
                .name = name,
                .is_open = true,
            };
        }
    
        fn close(self: *File) void {
            if (self.is_open) {
                std.debug.print("Closing {s}\n", .{self.name});
                self.is_open = false;
            }
        }
    
        fn read(self: File) ![]const u8 {
            if (!self.is_open) {
                return error.FileNotOpen;
            }
            return "file contents";
        }
    };
    
    fn processFile(path: []const u8) !void {
        var file = try File.open(path);
        defer file.close();
    
        const contents = try file.read();
        std.debug.print("Read: {s}\n", .{contents});
    }
    
    pub fn example() void {
        processFile("test.txt") catch |err| {
            std.debug.print("Failed to process file: {}\n", .{err});
        };
    }
    

    エラーのラッピング

    const std = @import("std");
    
    const LowLevelError = error{
        IOError,
        NetworkError,
    };
    
    const HighLevelError = error{
        ServiceUnavailable,
        DataCorrupted,
    };
    
    fn lowLevelOperation() LowLevelError!void {
        return LowLevelError.NetworkError;
    }
    
    fn highLevelOperation() (LowLevelError || HighLevelError)!void {
        lowLevelOperation() catch {
            // 低レベルエラーを高レベルエラーに変換
            return HighLevelError.ServiceUnavailable;
        };
    }
    
    pub fn example() void {
        highLevelOperation() catch |err| {
            std.debug.print("High level error: {}\n", .{err});
        };
    }
    

    エラーのログ記録

    const std = @import("std");
    
    const Logger = struct {
        fn logError(err: anyerror, context: []const u8) void {
            std.debug.print("[ERROR] {s}: {}\n", .{context, err});
        }
    };
    
    fn riskyDatabaseOperation() !i32 {
        return error.ConnectionFailed;
    }
    
    fn performOperation() !i32 {
        const result = riskyDatabaseOperation() catch |err| {
            Logger.logError(err, "Database operation failed");
            return err;
        };
        return result;
    }
    
    pub fn example() void {
        _ = performOperation() catch |err| {
            std.debug.print("Operation failed: {}\n", .{err});
        };
    }
    

    エラーのリトライ

    const std = @import("std");
    
    fn unreliableOperation(attempt: u32) !i32 {
        if (attempt < 3) {
            std.debug.print("Attempt {} failed\n", .{attempt});
            return error.TemporaryFailure;
        }
        return 42;
    }
    
    fn retryOperation(max_retries: u32) !i32 {
        var attempt: u32 = 0;
        while (attempt < max_retries) : (attempt += 1) {
            if (unreliableOperation(attempt)) |result| {
                std.debug.print("Success on attempt {}\n", .{attempt});
                return result;
            } else |err| {
                if (attempt == max_retries - 1) {
                    return err;
                }
                std.debug.print("Retrying...\n", .{});
            }
        }
        return error.MaxRetriesExceeded;
    }
    
    pub fn example() void {
        const result = retryOperation(5) catch |err| {
            std.debug.print("All retries failed: {}\n", .{err});
            return;
        };
        std.debug.print("Final result: {}\n", .{result});
    }
    

    エラー処理のベストプラクティス

    1. 具体的なエラーセットを使用

    const std = @import("std");
    
    // 良い例: 具体的なエラーセット
    const ValidationError = error{
        TooShort,
        TooLong,
        InvalidCharacter,
    };
    
    fn validateUsername(name: []const u8) ValidationError!void {
        if (name.len < 3) return ValidationError.TooShort;
        if (name.len > 20) return ValidationError.TooLong;
        // 検証ロジック
    }
    
    // 悪い例: anyerrorを多用
    fn validateEmail(email: []const u8) anyerror!void {
        _ = email;
        return error.SomeError;
    }
    
    pub fn example() void {
        validateUsername("ab") catch |err| {
            std.debug.print("Validation error: {}\n", .{err});
        };
    }
    

    2. errdeferでリソースを確実に解放

    const std = @import("std");
    
    fn allocateMultipleBuffers(allocator: std.mem.Allocator) !void {
        const buf1 = try allocator.alloc(u8, 100);
        errdefer allocator.free(buf1);
    
        const buf2 = try allocator.alloc(u8, 200);
        errdefer allocator.free(buf2);
    
        // エラーが発生してもリソースは解放される
        if (true) return error.ProcessingFailed;
    
        allocator.free(buf2);
        allocator.free(buf1);
    }
    
    pub fn example() void {
        const allocator = std.heap.page_allocator;
        allocateMultipleBuffers(allocator) catch |err| {
            std.debug.print("Error: {}\n", .{err});
        };
    }
    

    3. エラーを適切に伝播

    const std = @import("std");
    
    fn parseValue() !i32 {
        return error.ParseError;
    }
    
    fn processValue() !i32 {
        // tryでエラーを伝播
        const value = try parseValue();
        return value * 2;
    }
    
    fn handleValue() void {
        // トップレベルでエラーを処理
        const result = processValue() catch |err| {
            std.debug.print("Failed to handle value: {}\n", .{err});
            return;
        };
        std.debug.print("Result: {}\n", .{result});
    }
    
    pub fn example() void {
        handleValue();
    }
    

    まとめ

    この章では、Zigのエラー処理について学びました:

  • エラーユニオン型: 成功値とエラーを統一的に表現
  • try/catch: エラーの伝播と処理
  • errdefer: エラー時のリソース解放の自動化
  • スタックトレース: デバッグのための情報
  • ベストプラクティス: 具体的なエラーセット、適切なリソース管理
  • 次の章では、構造体と列挙型について学びます。

    参考資料

  • Zig Language Reference - Errors: https://ziglang.org/documentation/master/#Errors
  • Ziglearn - Error Handling: https://ziglearn.org/chapter-1/#errors
  • Zig Standard Library - Error Utilities: https://ziglang.org/documentation/master/std/