Rust中的错误处理
Error handling in Rust

原始链接: https://felix-knorr.net/posts/2025-06-29-rust-error-handling.html

为模块或板条箱定义大型、包罗万象的错误枚举的标准Rust错误处理模式引入了一个类型安全问题:函数经常返回它们无法产生的错误,需要手动过滤。虽然实用,但这破坏了Rust的类型驱动正确性方法。 替代方法旨在更精确地定义错误。一种方法,以“terrors”板条箱为例,使用结构来处理单个错误,并将它们组合成集合,尽管转换可能会很冗长。`error_set`板条箱使用宏、定义错误枚举和自动生成转换特征提供了一种更简洁的解决方案。它允许组合错误变量和集合,从而启用“?`当错误集是子集时,操作员可以无缝工作。虽然对于基于结构的错误仍然有点冗长,但它在精度和实用性之间提供了平衡。其他板条箱也探索了类似的范式,如“SmartErr”,甚至还有基于函数体动态生成错误枚举的解决方案。

这篇Hacker News帖子讨论了Rust中的错误处理策略,这是由一篇提倡更细粒度错误类型的文章引发的。主要的争论集中在每个模块/库使用一种错误类型(感知的“现状”)还是每个函数/操作使用一种故障类型。 那些支持特定于函数的错误的人认为,它提供了更好的类型安全性、文档,并强制进行全面的错误处理,尽管由于样板和在引入新错误时需要更新调用者层次结构,它可能很麻烦。像“thiserror”和“snafu”这样的库被称为简化这一过程的工具。 反驳者强调了错误扩散的可能性,以及对更简单、更统一的错误处理的渴望,其中“无论如何”等工具更适合应用程序级代码。一些评论者将其与Java中的检查异常进行了比较,并讨论了Rust宏重方法的优缺点。还有一个关于Rust中缺少联合类型以及它们如何简化错误处理的讨论。
相关文章

原文

The current standard for error handling, when writing a crate, is to define one error enum per module, or one for the whole crate that covers all error cases that the module or crate can possibly produce, and each public function that returns a Result will use said error enum.

This means, that a function will return an error enum, containing error variants that the function cannot even produce. If you match on this error enum, you will have to manually distinguish which of those variants are not applicable in your current scope, based on the documentation of the function (and who reads that anyway? /s).

The problem with the status quo

What makes Rust so great, is the ability to express requirements via the type system in a way that makes it very hard for you to violate them, and yet, we collectively decided to create these huge error-enums. I completely understand where this is coming from. Defining an extra error enum for every function and all the conversions between them is extremely tedious. And so everyone and their mother is building big error types. Well, not Everyone. A small handful of indomitable nerds still holds out against the standard.

The alternative

An error is a singular bit of information, might be completely independent of other errors a function can return, and should probably be represented by a struct rather than an enum variant. A function returns one of a set of those if it goes wrong, but it doesn't define the errors themselves. The first Rust crate I saw that followed this philosophy, was terrors (Go ahead, check it out). I still think it's beautiful. It's also a little inconvenient. You have to write .map_err(OneOf::broaden) a lot and some functions have a lot of possible error points, some of which being the contents of other function's error sets. And yet, you have to spell them out all over again. Still, I really like this crate ... from a distance.

My personal favorite

Speaking of error sets, there is a crate with this name, that I prefer to use nowadays. Instead of doing Olympia level type acrobatics (like terrors) it uses macros. It allows you to define error enums for different functions in a very concise way and automatically generates the trait implementations for conversions between those. Want a taste?

error_set! {
    BtlePlug = {
        BtlePlug(btleplug::Error)
    };

    FindSDeviceError = { BLENoAdapter, Timeout, NoSDevice } || BtlePlug || FilterSDeviceError;
    FilterSDeviceError = { BLEAdapterDisconnect, Timeout } || BtlePlug;
    ConnectToSDeviceError = {NoRxChar, NoTxChar, NoKaChar} || BtlePlug;
    ConnectAndRunError = FindSDeviceError
                        || ConnectToSDeviceError
                        || ForwardToMainThreadError
                        || ForwardToSDeviceError;
                        || BtlePlug
    ForwardToSDeviceError = {MainThreadDied, } || BtlePlug;
    ForwardToMainThreadError = {SendError(mpsc::SendError<Vec<u8>>)};

    DecoderError = {Invalid,};

    #[derive(PartialEq)]
    CrcError = { CrcMissmatch {actual: u16, expected: u16}, ConversionError };
}

It allows us to create error sets from variants and from unions with other error sets. The ? operator will work if the error set you use it on is a sub-set of the function's error set, and it will find out whether that's the case, even if you don't use the union operator, i.e. this works:

error_set! {
    A = { Foo, Bar, Baz };
    B = { Foo, Bar };
}

fn b() -> Result<(), B> {
    Err(B::Foo)
}

fn a() -> Result<(), A> {
    b()?;
    Ok(())
}

This is still a bit too verbose for my tastes if you use many actual struct errors, e.g. because you want some fields on them to carry additional information, or because you want to annotate them with error messages. However, I need them seldomly enough, so that I'll happily pay the extra keystrokes to define a wrapper enum for them (like the BtlePlug enum in the first example) for now.

There are more libraries out there that explore this paradigm in different ways, e.g. SmartErr. And I once saw a crate that offered an attribute macro that you could slap on a function, and then it would parse the functions body and generate an error enum and insert it into the functions return type, based on the errors that occured in the function's body. Sadly I didn't find it again despite searching for it for an hour. If anyone has a link, please tell me.

联系我们 contact @ memedata.com