解答9: エラーハンドリングの完全理解

概要

この解答では、Zigのエラーハンドリングシステムについて学びます。Zigは例外を使用せず、エラーを戻り値として明示的に扱います。

Part 1: エラーセット

const std = @import("std");

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

const ParseError = error{
    InvalidFormat,
    UnexpectedToken,
    EndOfInput,
};

// エラーセットの合成
const ConfigError = FileError || ParseError;

fn readFile(path: []const u8) FileError![]const u8 {
    if (std.mem.eql(u8, path, "missing.txt")) {
        return FileError.NotFound;
    }
    return "file content";
}

Part 2: try と catch

const std = @import("std");

const ComputeError = error{
    DivisionByZero,
    Overflow,
};

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

fn safeCompute(a: i32, b: i32) i32 {
    // catch でデフォルト値
    return divide(a, b) catch 0;
}

fn chainedComputation(values: []const i32) ComputeError!i32 {
    var result: i32 = 0;
    for (values) |v| {
        result = try divide(result + v, 2);
    }
    return result;
}

Part 3: errdefer

const std = @import("std");

fn createResources(allocator: std.mem.Allocator) !struct { r1: []u8, r2: []u8 } {
    var r1 = try allocator.alloc(u8, 100);
    errdefer allocator.free(r1);

    var r2 = try allocator.alloc(u8, 200);
    errdefer allocator.free(r2);

    return .{ .r1 = r1, .r2 = r2 };
}

ポイント解説

  • 明示性: すべてのエラーは型システムで表現
  • ゼロコスト: 正常パスにオーバーヘッドなし
  • コンパイル時チェック: エラーの未処理をコンパイルエラーに
  • よくある間違い

    // 危険!エラーを無視
    _ = mightFail() catch {};
    
    // 適切: エラーをログ
    mightFail() catch |err| {
        std.debug.print("Error: {}\n", .{err});
    };
    

    自己確認チェックリスト

  • [ ] エラーセットの定義と合成を理解しているか
  • [ ] try/catchの使い分けを説明できるか
  • [ ] errdeferの実行タイミングを把握しているか