第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のエラー処理について学びました:
次の章では、構造体と列挙型について学びます。