On Error Handling in Rust
2025-06-29
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).
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.
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.
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.