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:
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:
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:
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:
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:
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
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
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
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
- ✅
anyerrortype 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.