Skip to content

Zig 错误处理教程

错误集类似于枚举,每个错误名称在编译时被分配一个大于0的整数。可以多次声明相同的错误名,它们会获得相同的整数值。

// 定义错误集
const FileError = error{
AccessDenied,
OutOfMemory,
FileNotFound,
};
// 默认使用 u16 类型存储
// 可通过 --error-limit [num] 参数指定最大错误数
const FileOpenError = error{ AccessDenied, OutOfMemory, FileNotFound };
const AllocationError = error{ OutOfMemory };
fn foo(err: AllocationError) FileOpenError {
return err; // ✅ 允许:子集 → 超集
}
fn bar(err: FileOpenError) AllocationError {
return err; // ❌ 编译错误:超集 → 子集
// 错误信息会显示哪些错误不在目标错误集中
}
const err = error.FileNotFound;
// 等价于:
const err = (error{FileNotFound}).FileNotFound;

anyerror 表示全局错误集,包含整个编译单元中的所有错误。

// 任何错误集都可以强制转换为 anyerror
fn returnsAnyError() anyerror!u32 {
return error.SomeError;
}
// 也可以从 anyerror 显式转换到特定错误集
fn explicitCast(err: anyerror) !FileError {
// 编译器会插入断言确保错误值在目标错误集中
return @errSetCast(FileError, err);
}

尽量避免使用 anyerror,因为:

  • 编译器无法在编译时知道可能的错误
  • 影响生成文档和错误消息
  • 在 switch 中容易遗漏错误处理

使用 ! 操作符将错误集与正常类型组合:

// 解析字符串为 u64,可能返回错误
fn parseU64(buf: []const u8, radix: u8) !u64 {
if (buf.len == 0) return error.EmptyString;
// ... 解析逻辑
return result;
}

可以使用编译期反射访问错误联合的子类型:

test "error union reflection" {
var foo: anyerror!i32 = undefined;
// 访问负载类型
try expect(@typeInfo(@TypeOf(foo)).error_union.payload == i32);
// 访问错误集类型
try expect(@typeInfo(@TypeOf(foo)).error_union.error_set == anyerror);
}
const number = parseU64(str, 10) catch 0;
const number = parseU64(str, 10) catch blk: {
// 复杂错误处理逻辑
std.debug.print("解析失败,使用默认值\n", .{});
break :blk 42;
};

try 是错误传播的快捷语法:

// 原始写法
const number = parseU64(str, 10) catch |err| return err;
// 使用 try 的简洁写法
const number = try parseU64(str, 10);

如果确定表达式不会返回错误,使用 catch unreachable:

// 我们知道 "1234" 一定能成功解析
const number = parseU64("1234", 10) catch unreachable;
// 在 Debug 和 ReleaseSafe 模式下,如果出错会触发安全恐慌
fn handleAllErrors(str: []u8) void {
if (parseU64(str, 10)) |number| {
// 成功情况
doSomething(number);
} else |err| switch (err) {
error.Overflow => {
// 处理溢出
handleOverflow();
},
error.InvalidChar => {
// 处理无效字符
handleInvalidChar();
},
}
}
fn handleSomeErrors(str: []u8) error{InvalidChar}!void {
if (parseU64(str, 10)) |number| {
doSomething(number);
} else |err| switch (err) {
error.Overflow => {
// 只处理溢出错误
handleOverflow();
},
else => |leftover| return leftover,
// 其他错误传播出去
}
}
fn ignoreErrorDetails(str: []u8) void {
if (parseU64(str, 10)) |number| {
doSomething(number);
} else |_| {
// 忽略具体错误
handleGenericFailure();
}
}

errdefer 只在函数返回错误时执行:

fn createResource(param: i32) !Resource {
const resource = try allocateResource();
errdefer deallocateResource(resource); // 只在失败时清理
const buffer = try allocateBuffer() orelse return error.OutOfMemory;
defer deallocateBuffer(buffer); // 总是清理
if (param > 1000) return error.InvalidParam;
// 如果成功返回,errdefer 不会执行
return resource;
}
fn captureError(captured: *?anyerror) !void {
errdefer |err| {
captured.* = err; // 捕获错误
}
return error.SomeError;
}

使用 || 操作符合并错误集:

const A = error{ NotDir, PathNotFound };
const B = error{ OutOfMemory, PathNotFound };
const C = A || B; // 包含:NotDir, PathNotFound, OutOfMemory
// 左侧文档注释会覆盖右侧
// PathNotFound 使用 A 的文档注释

在函数返回类型前使用 ! 可以推断错误集:

// 推断错误集
fn addInferred(a: u32, b: u32) !u32 {
const ov = @addWithOverflow(a, b);
if (ov[1] != 0) return error.Overflow;
return ov[0];
}
// 显式错误集
fn addExplicit(a: u32, b: u32) Error!u32 {
const ov = @addWithOverflow(a, b);
if (ov[1] != 0) return error.Overflow;
return ov[0];
}
const Error = error{ Overflow };

注意:

  • 推断错误集会使函数变成泛型
  • 可能影响函数指针使用
  • 不兼容递归
  • 建议从空错误集开始,根据编译器提示完善

7. 错误返回跟踪 (Error Return Traces)

Section titled “7. 错误返回跟踪 (Error Return Traces)”

显示错误从发生点传播到最终捕获点的完整路径:

pub fn main() !void {
try foo(12); // 错误从这里开始传播
}
fn foo(x: i32) !void {
try bar(); // 传递错误
}
fn bar() !void {
return error.PermissionDenied; // 错误源
}

输出示例:

error: PermissionDenied
file.zig:15:5: 0x113d36c in bar
return error.PermissionDenied;
^
file.zig:7:9: 0x113d654 in foo
try bar();
^
file.zig:2:5: 0x113d71b in main
try foo(12);
^
特性错误返回跟踪堆栈跟踪
显示内容错误传播路径函数调用栈
控制流显示错误处理逻辑只显示调用关系
调试价值更高(显示错误转换)较低
  • 在 Debug 构建中默认启用
  • 在 Release 构建中默认禁用
  • 可通过以下方式访问:
    const trace = @errorReturnTrace();
    if (trace) |t| {
    std.debug.dumpStackTrace(t.*);
    }
// 每个可能返回错误的函数接收一个秘密参数:
// stack_trace: *StackTrace
// 无错误时只有一个内存写操作
// 用于初始化栈跟踪结构体
// 返回错误前调用:
fn __zig_return_error(stack_trace: *StackTrace) void {
stack_trace.instruction_addresses[stack_trace.index] = @returnAddress();
stack_trace.index = (stack_trace.index + 1) % N;
}
// 成本:2次数学操作 + 内存读写
// 内存访问局限,通常会在缓存中
  • 无错误返回跟踪:普通返回语句
  • 有错误返回跟踪:函数调用或跳转指令
  • 计划优化为尾调用,使代码大小成本为零
  1. 优先使用具体错误集,避免 anyerror
  2. 合理使用 try 简化错误传播
  3. 配合 errdefer 确保资源清理
  4. 完整处理错误,或显式传播
  5. 利用错误返回跟踪进行调试
  6. 从空错误集开始,逐步完善
  7. 合并相关错误集,提高代码复用性
const std = @import("std");
// 定义业务错误集
const DatabaseError = error{
ConnectionFailed,
QueryTimeout,
DataCorrupted,
};
const NetworkError = error{
Timeout,
ConnectionReset,
ProtocolError,
};
const AppError = DatabaseError || NetworkError || error{
InvalidInput,
ResourceExhausted,
};
fn fetchData(query: []const u8) AppError!Data {
const conn = try connectToDatabase() catch |err| {
std.log.err("数据库连接失败: {}", .{err});
return error.ConnectionFailed;
};
defer conn.close();
errdefer |err| {
std.log.err("查询失败: {}, 查询: {s}", .{ err, query });
};
const result = try conn.execute(query);
return try parseResult(result);
}
pub fn main() void {
const data = fetchData("SELECT * FROM users") catch |err| {
std.debug.print("应用程序错误: {}\n", .{err});
if (@errorReturnTrace()) |trace| {
std.debug.dumpStackTrace(trace.*);
}
std.process.exit(1);
};
processData(data);
}

通过掌握 Zig 的错误处理系统,你可以编写出既安全又高效的代码,同时获得优秀的调试体验。