Error handling in Rust
88 comments
·June 29, 2025slau
WhyNotHugo
> I disagree that the status quo is “one error per module or per library”.
It is the most common approach, hence, status quo.
> I create one error type per function/action. I discovered this here on HN after an article I cannot find right now was posted.
I like your approach, and think it's a lot better (including for the reasons described in this article). Sadly, there's still very few of us taking this approach.
shepmaster
> I create one error type per function/action
I do too! I've been debating whether I should update SNAFU's philosophy page [1] to mention this explicitly, and I think your comment is the one that put me over the edge for "yes" (along with a few child comments). Right now, it simply says "error types that are scoped to that module", but I no longer think that's strong enough.
[1]: https://docs.rs/snafu/latest/snafu/guide/philosophy/index.ht...
edbaskerville
I thought I was a pedantic non-idiomatic weirdo for doing this. But it really felt like the right way---and also that the language should make this pattern much easier.
resonious
The "status quo" way erodes the benefit of Rust's error system.
The whole point (in my mind at least) of type safe errors is to know in advance all if the failure modes of a function. If you share an error enum across many functions, it no longer serves that purpose, as you have errors that exist in the type but are never returned by the function.
It would be nice if the syntax made it easier though. It's cumbersome to create new enums and implement Error for each of them.
mananaysiempre
> also that the language should make this pattern much easier
Open sum types? I’m on the fence as to whether they should be inferrable.
YorickPeterse
I suspect you're referring to this article, which is a good read indeed: https://mmapped.blog/posts/12-rust-error-handling
atombender
With that scheme, what about propagating errors upwards? It seems like you have to wrap all "foreign" error types that happen during execution with an explicit enum type.
For example, let's say the function frobnicate() writes to a file and might get all sorts of file errors (disk full, no permissions, etc.). It seems like those have to be wrapped or embedded, as the article suggests.
But then you can't use the "?" macro, I think? Because wrapping or embedding requires constructing a new error value from the original one. Every single downstream error coming from places like the standard library has to go through a transformation with map_err() or similar.
remram
"?" call "into()" automatically, which covers simple wrapping.
mananaysiempre
It’s a nice, well-written and well-reasoned article, and yet after shoveling through all the boilerplate it offers as the solution I can’t help but “WTF WTF WTF ...”[1].
larusso
Have an example that I can read. I also use this error but struggle a bit when it comes to deciding how fine grained or like in the article, how big an error type should be.
LoganDark
`thiserror` is a derive macro :)
thrance
That's the way, but I find it quite painful at time. Adding a new error variant to a function means I now have to travel up the hierarchy of its callers to handle it or add it to their error set as well.
layer8
This is eerily reminiscent of the discussions about Java’s checked exceptions circa 25 years ago.
mananaysiempre
The difference is that Java does not have polymorphism for exception sets (I think? certainly it didn’t for anything back then), so you couldn’t even type e.g. a callback-invoking function properly. Otherwise, yes, as well as the discussions about monad transformers in Haskell from 15 years ago. Effects are effects are effects.
slau
This shouldn’t happen unless you’re actively refactoring the function and introducing new error paths. Therefore, it is to be expected that the cake hierarchy would be affected.
You would most likely have had to navigate up and down the caller chain regardless of how you scope errors.
At least this way the compiler tells you when you forgot to handle a new error case, and where.
mdaniel
Sometimes that error was being smuggled in another broader error to begin with, so if the caller is having to go spelunking into the .description (or .message) to know, that's a very serious problem. The distinction I make is: if the caller knew about this new type of error, what would they do differently?
Animats
It's hard. Python 2.x had a good error exception hierarchy, which made it possible to sort out transient errors (network, remote HTTP, etc.) errors from errors not worth retrying. Python 3 refactored the error hierarchy, and it got worse from the recovery perspective, but better from a taxonomy perspective.
Rust probably should have had a set of standard error traits one could specialize, but Rust is not a good language for what's really an object hierarchy.
Error handling came late to Rust. It was years before "?" and "anyhow". "Result" was a really good idea, but "Error" doesn't do enough.
efnx
`Result` is just `Either` by another name, and the main idea is to use sum and product types as the result of a computation instead of throwing, which turns error handling into business as usual. The `Result` type and `Error` trait really are orthogonal, and as soon as the `Try` trait is stabilized I think we'll see some good improvements.
tialaramex
Semantics matter. This is a mistake C++ has made and will probably pay for when it eventually tries to land pattern matching. Rust knows that Option<Goose> and Result<Goose,()> are completely different semantically, likewise Result<Happy,Sad> and Either<Happy,Sad> communicate quite different intents even if the in-memory representations are identical.
_benton
I really wish Rust had proper union types. So much ceremony over something that could be Foo | Bar | Error
amluto
I don't, and I say this as a long-time user of C++'s std::variant and boost::variant, which are effectively union types.
Foo | Bar makes sense when Foo and Bar are logically similar and their primary difference is the difference in type. This is actually rather rare. One example would be a term in your favorite configuration markup language along the lines of JSON or YAML:
type Term = String | List<String>;
or perhaps a fancier recursive one: type Term = String | Box<List<Term>>;
or however you want to spell it. Here a Term is something in the language, and there are two kinds of terms: string or lists.But most of the time that I've wanted a sum type, I have a set of logical things that my type can represent, and each of those things has an associated type of the data they carry. Result types (success or error) are absolutely in that category. And doing this wrong can result in a mess. For example, if instead of Result, you have SuccessVal | Error, then the only way to distinguish success from error is to literally, or parametrically in a generic, spell out SuccessVal or Error. And there are nasty pathological cases, for example, what if you want a function that parses a string into an Error? You would want to write:
fn parse_error(input_string: &str) -> Error | Error
Whoops!kelnos
Yeah, it's frustrating that there's no syntax for this. It could even be syntactic sugar; in this case if you had:
type FooOrBarOrError = Foo | Bar | Error;
Then that could desugar to: enum FooOrBarOrError {
Foo(Foo),
Bar(Bar),
Error(Error),
}
And it could also implement From for you, so you can easily get a FooOrBarOrError from a Foo, Bar, or Error; as well as implementing Display, StdError, etc. if the components already implement them.I actually wonder if you could implement this as a proc macro...
rtpg
I've thought about this a good amount too because Typescript gets so much out of untagged unions but I think that with a language like Rust you get into a huge mess due to it messing up inference and also Rust _really_ wanting to know the size of a type most of the time.
let mut x = 1
x = false
is x a usize? a bool? a usize | bool? let mut x = if some_condition { 1 } else { false }
is x x a usize? a bool? a usize | bool?One could make inference rules about never inserting unions without explicit intervention of user types. But then you get into (IMO) some messy things downstream of Rust's pervasive "everything is an expression" philosophy.
This is less of a problem in Typescript because you are, generally, much less likely to have conditionally typed expressions. There's a ternary operator, but things like `switch` are a statement.
So in production code Typescript, when presented with branching, will have a downstream explicit type to unify on. Not so much in Rust's extensive type chaining IMO. And then we start talking about Into/From and friends....
I don't really think that you want a rule like `(if cond { x: T } else { y: U}) : T | U` in general. You'll end up with _so many_ false negatives and type errors at a distance. But if you _don't_ have that rule, then I don't know how easily your error type unification would work.
metaltyphoon
Look at Zig then, as it does exactly this. However you can’t carry any context and it’s also a problem.
jonstewart
There’s much not to like about C++ exceptions, I get it, but as a C++ programmer the proliferation of error types in Rust rubs me the wrong way. I like that C++ defines a hierarchy of exceptions, like Python, and you are free to reuse them. I do not want to go to the hassle of defining some new error types everywhere, I just want to use equivalents to runtime_error or logic_error. It feels like Rust is multiplying unnecessarily.
mparis
I'm a recent snafu (https://docs.rs/snafu/latest/snafu/) convert over thiserror (https://docs.rs/thiserror/latest/thiserror/). You pay the cost of adding `context` calls at error sites but it leads to great error propagation and enables multiple error variants that reference the same source error type which I always had issues with in `thiserror`.
No dogma. If you want an error per module that seems like a good way to start, but for complex cases where you want to break an error down more, we'll often have an error type per function/struct/trait.
shepmaster
Thanks for using SNAFU! Any feedback you'd like to share?
pjmlp
This is the kind of stuff I would rather not have outsourced for 3rd party dependencies.
Every Rust project starts by looking into 3rd party libraries for error handling and async runtimes.
wongarsu
Or rather every rust project starts with cargo install tokio thiserror anyhow.
If we just added what 95% of projects are using to the standard library then the async runtime would be tokio, and error handling would be thiserror for making error types and anyhow for error handling.
Your ability to go look for new 3rd party libraries, as well as this article's recommendations, are examples of how Rust's careful approach to standard library additions allows the ecosystem to innovate and try to come up with new and better solutions that might not be API compatible with the status quo
pjmlp
I rather take the approach that basic language features are in the box.
Too much innovation gets out of control, and might not be available every platform.
nemothekid
I think this is a valid criticism, however I think the direction Rust went was better. It's easy in hindsight to say that the error handling system that emerged in ~2020-ish should have been baked in the library when the language was stabilized in 2015. However even in 2012 (when Go 1.0 was released), errors as values was a pretty novel idea among mainstream programming languages and Go has some warts that were baked into the language that they have now given up on fixing.
As a result, I find error handling in Go to be pretty cumbersome even though the language design has progressed to a point where it theoretically could be made much more ergonomic. You can imagine a world where instead of functions returning `(x, err)` they could return `Result[T]error` - an that would open up so many more monadic apis, similar to whats in Rust. But that future seems to be completely blocked off because of the error handling patterns that are now baked into the language.
There's no guarantee the Rust team would have landed on something particularly useful. Even the entire error trait, as released, is now deprecated. `thiserror`, the most popular error crate for libraries wasn't released until 2019.
jenadine
I prefer `derive_more` than thiserror. Because it is a superset and has more useful derive I use.
color-eyre is better than anyhow.
johnisgood
Just please let us not end up with something like Node.js where we use a crate that has <10 LOC. Irks me. And if Rust ends up like that, I will never switch. It already builds ~50 crates for medium-sized projects.
nixpulvis
As a bit of an aside, I get pretty far just rolling errors by hand. Variants fall into two categories, wrappers of an underlying error type, or leafs which are unique to my application.
For example,
enum ConfigError {
Io(io::Error),
Parse { line: usize, col: usize },
...
}You could argue it would be better to have a ParserError type and wrap that, and I absolutely might do that too, but they are roughly the same and that's the point. Move the abstraction into their appropriate module as the complexity requests it.
Pretty much any error crate just makes this easier and helps implement quality `Display` and other standard traits for these types.
echelon
> I get pretty far just rolling errors by hand
And you don't punish your compile times.
The macro for everything folks are making Rust slow. If we tire of repetition, I'd honestly prefer checked in code gen. At least we won't repeatedly pay the penalty.
devnullbrain
>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
You have to anyway.
The return type isn't to define what error variants the function can return. We already have something for that, it's called the function body. If we only wanted to specify the variants that could be returned, we wouldn't need to specify anything at all: the compiler could work it out.
No. The point of the function signature is the interface for the calling function. If that function sees an error type with foo and bar and baz variants, it should have code paths for all of them.
It's not right to say that the function cannot produce them, only that it doesn't currently produce them.
the__alchemist
Lately, I've been using io::Error for so many things. (When I'm on std). It feels like everything on my project that has an error that I could semantically justify as I/O. Usually it's ErrorKind::InvalidData, even more specifically. Maybe due to doing a lot of file and wire protocol/USB-serial work?
On no_std, I've been doing something like the author describes: Single enum error type; keeps things simple, without losing specificity, due the variants.
When I need to parse a utf-8 error or something, I use .map_err(|_| ...)
After reading the other comments in this thread, it sounds like I'm the target audience for `anyhow`, and I should use that instead.
nixpulvis
I think I read somewhere that anyhow is great for application code where you want a unified error type across the application. And something like thiserror is good for library code where you want specific error variants for each kind of fallibility.
Personally, I think I prefer thiserror style errors everywhere, but I can see some of the tradeoffs.
benreesman
The best parts of Rust are Haskell. You've got a lot of precedent for how you do it.
xixixao
I find TS philosophy of requiring input types and inferring return types (something I was initially quite sceptical about when Flow was adopting it) quite nice to work with in practice - the same could be applied to strict typing of errors ala Effect.js?
This does add the “complexity” of there being places (crate boundaries in Rust) where you want types explicitly defined (so to infer types in one crate doesn’t require typechecking all its dependencies). TS can generate these types, and really ought to be able to check invariants on them like “no implicit any”.
Rust of course has difference constraints and hails more from Haskell’s heritage where the declared return types can impact runtime behavior instead. I find this makes Rust code harder to read unfortunately, and would avoid it if I could in Rust (it’s hard given the ecosystem and stdlib).
estebank
Fun fact: the compiler itself has some limited inference abilities for return types, they are just not exposed to the language: https://play.rust-lang.org/?version=nightly&mode=debug&editi...
I have some desire to make an RFC for limited cross-item inference within a single crate, but part of it wouldn't be needed with stabilized impl Trait in more positions. For public items I don't think the language will ever allow it, not only due to technical concerns (not wanting global inference causing compile times to explode) but also language design concerns (inferred return types would be a very big footgun around API stability for crate owners).
xixixao
This already does work in TS, and there are some patterns besides Effect that simplify working with the return values.
Which brings me to my other big gripe with Rust (and Go): the need to declare structs makes it really unwieldy to return many values (resorting to tuples, which make code more error prone and again harder to read).
_benton
Yep. I wish Rust supported proper union types. Typescript gets it right, I just don't want to be writing Javascript...
IshKebab
Kind of reminds me of Java checked exceptions.
kshri24
> 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 author is a fan of Asterix I see :)
metaltyphoon
> The current standard for error handling, when writing a crate, is to define one error enum per module…
Excuse me what?
> This means, that a function will return an error enum, containing error variants that the function cannot even produce.
The same problem happens with exceptions.
jppittma
I don’t really agree with this. The vast majority of the time, if you encounter an error at runtime, there’s not much you can do about it, but log it and try again. From there, it becomes about bubbling the error up until you have the context to do that. Having to handle bespoke error type from different libraries is actually infuriating, and people thinking this is a good idea makes anyhow mandatory for development in the language.
I disagree that the status quo is “one error per module or per library”. I create one error type per function/action. I discovered this here on HN after an article I cannot find right now was posted.
This means that each function only cares about its own error, and how to generate it. And doesn’t require macros. Just thiserror.