[HN Gopher] Error handling in Rust
       ___________________________________________________________________
        
       Error handling in Rust
        
       Author : emschwartz
       Score  : 68 points
       Date   : 2025-06-29 20:28 UTC (2 hours ago)
        
 (HTM) web link (felix-knorr.net)
 (TXT) w3m dump (felix-knorr.net)
        
       | jgilias wrote:
       | Yeah... Please no.
       | 
       | I'm getting a bit of a macro fatigue in Rust. In my humble
       | opinion the less "magic" you use in the codebase, the better.
       | Error enums are fine. You can make them as fine-grained as makes
       | sense in your codebase, and they end up representing a kind of an
       | error tree. I much prefer this easy to grok way to what's
       | described in the article. I mean, there's enough things to think
       | about in the codebase, I don't want to spend mental energy on
       | thinking about a fancy way to represent errors.
        
         | Waterluvian wrote:
         | Agreed about magic.
         | 
         | Please correct me if I'm misunderstanding this, but something
         | that surprised me about Rust was how there wasn't a guaranteed
         | "paper trail" for symbols found in a file. Like in TypeScript
         | or Python, if I see "Foo" I should 100% expect to see "Foo"
         | either defined or imported in that specific file. So I can
         | always just "walk the paper trail" to understand where
         | something comes from.
         | 
         | Or I think there was also a concept of a preamble import? Where
         | just by importing it, built-ins and/or other things would gain
         | additional associated functions or whatnot.
         | 
         | In general I just really don't like the "magic" of things being
         | within scope or added to other things in a manner that it's not
         | obvious.
         | 
         | (I'd love to learn that I'm just doing it wrong and none of
         | this is actually how it works in Rust)
        
           | jgilias wrote:
           | You can import everything from a module with a *, but most
           | people seem to prefer to import things explicitly. But, yes,
           | you can generally figure out easily where things are coming
           | from!
        
           | itishappy wrote:
           | Doesn't something like the following break the trail in
           | pretty much all languages?                   from package
           | import *
        
             | Waterluvian wrote:
             | Yup. And I've not seen that used in forever and it's often
             | considered a linting error because it is so nasty.
             | 
             | So maybe what I'm remembering about Rust was just seeing a
             | possible but bad convention that's not really used much.
        
           | 57473m3n7Fur7h3 wrote:
           | It's a bit confusing sometimes with macros that create types
           | that don't seem to exist. But usually when I work with code I
           | use an IDE anyway and "go to definition" will bring me to
           | where it's defined, even when it's via a macro.
           | 
           | Still generally prefer the plain non-macro declarations for
           | structs and enums though because I can easily read them at a
           | glance, unlike when "go to definition" brings me to some
           | macro thing.
        
         | burnt-resistor wrote:
         | Yes. Macros are a hammer, but not everything is a nail.
         | 
         | Declarative macros (macro_rules) should be used to
         | straightforwardly reduce repetitive, boilerplate code
         | generation and making complex, messy things simpler.
         | 
         | Procedural macros (proc_macro) allow creating arbitrary,
         | "unhygienic" code that declarative macros forbid and also
         | custom derive macros and such.
         | 
         | But it all breaks down when use of a library depends too much
         | on magic code generation that cannot be inspected. And now
         | we're back to dynamic language (Ruby/Python/JS) land with
         | opaque, tinkering-hostile codebases that have baked-in
         | complexity and side-effects.
         | 
         | Use magic where appropriate, but not too much of it, is often
         | the balance that's needed.
        
           | quotemstr wrote:
           | > Yes. Macros are a hammer, but not everything is a nail.
           | 
           | Overuse of macros is a symptom of missing language
           | capabilities.
           | 
           | My biggest disappointment in Rust (and probably my least
           | popular opinion) is how Rust botched error handling. I think
           | non-local flow control (i.e. exceptions) with automated
           | causal chaining (like Python) is a good language design point
           | and I think Rust departed from this good design point
           | prematurely in a way that's damaged the language in unfixable
           | ways.
           | 
           | IOW, Rust should have had _only_ panics, and panic objects
           | should have had rich contextual information, just like Java
           | and Python. There should also have been an enforced "does not
           | panic" annotation like noexcept in C++. And Drop
           | implementations should not be allowed to panic. Ever.
           | 
           | God, I hope at least yeet gets in.
        
             | zbentley wrote:
             | > Rust should have had _only_ panics, and panic objects
             | should have had rich contextual information, just like Java
             | and Python.
             | 
             | It could have gone that way, but that would have "fattened"
             | the runtime and overhead of many operations, making rust
             | unsuitable for some low-overhead-needed contexts that it
             | chose to target as use-cases. More directly: debug and
             | stack info being tracked on each frame has a cost (as it
             | does in Java and many others). So does reassembling that
             | info by taking out locks and probing around the stack to
             | reassemble a stack trace (C++). Whether you agree with
             | Rust's decision to try to serve those low-overhead niches
             | or not, that (as I understand it) is a big part of the
             | reason for why errors work the way they do.
             | 
             | > There should also have been an enforced "does not panic"
             | annotation like noexcept in C++. And Drop implementations
             | should not be allowed to panic.
             | 
             | I sometimes think that I'd really love "nopanic". Then I
             | consider everything that could panic (e.g. allocating) and
             | I like it less. I think that heavy use of such a feature
             | would lead to people just giving up and calling abort() in
             | library code in order to be nopanic-compatible, which is an
             | objectively worse outcome than what we have today.
        
         | throwaway894345 wrote:
         | Both sides have been a pain for me. Either I'm debugging macro
         | errors or else I'm writing boilerplate trait impls all day...
         | It feels like a lose/lose. I have yet to find a programming
         | language that does errors well. :/
        
       | fwip wrote:
       | This looks nice, especially for a mature/core library.
       | 
       | If your API already maps to orthogonal sets of errors, or if it's
       | in active development/iteration, you might not get much value
       | from this. But good & specific error types are great
       | documentation for helping developers understand "what can go
       | wrong," and the effects compound with layers of abstraction.
        
         | jgilias wrote:
         | Is it really though? What's the point of having an error type
         | per function? As the user of std::io, I don't particularly care
         | if file couldn't be read in function foo, bar, or baz, I just
         | care that the file couldn't be read.
        
           | dwattttt wrote:
           | An error type per function doubles as documentation. If you
           | treat all errors as the same it doesn't matter, but if you
           | have to handle some, then you really care about what actual
           | errors a function can return.
        
             | jgilias wrote:
             | Ok, that's a valid point! Though there's a trade-off there,
             | right? If both bar and baz can not find a file, they're
             | both going to return their own FileNotFound error type. And
             | then, if you care about handling files not being found
             | somewhere up the stack, don't you now have to care about
             | two error types that both represent the same failure
             | scenario?
        
               | Groxx wrote:
               | if you have multiple files that are read in a function,
               | and they might lead to different error handling... then
               | sometimes yeah, perhaps they should be different types so
               | you are guaranteed to know that this is a possibility,
               | and can choose to do something specific when X happens.
               | or to be informed if the library adds another file to the
               | mix.
               | 
               | it isn't always the case, of course, but it also isn't
               | always NOT the case.
        
               | dwattttt wrote:
               | A framing about the problem I don't often see is: when do
               | you want to throw away information about an error case?
               | Losing that information is sometimes the right thing to
               | do (as you said, maybe you don't care about which file
               | isn't found by a function, so you only use one error to
               | represent the two times it can happen).
               | 
               | Maybe it would make sense to consider the API a function
               | is presenting when making errors for it; if an error is
               | related to an implementation detail, maybe it doesn't
               | belong in the public API. If an error does relate to the
               | public function's purpose (FileNotFound for a function
               | that reads config), then it has a place there.
        
       | burnt-resistor wrote:
       | To skip recreating OOP in Rust, use anyhow instead.
        
       | slau wrote:
       | 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.
        
         | thrance wrote:
         | 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.
        
           | mdaniel wrote:
           | 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?
        
           | slau wrote:
           | 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.
        
           | layer8 wrote:
           | This is eerily reminiscent of the discussions about Java's
           | checked exceptions circa 25 years ago.
        
         | edbaskerville wrote:
         | 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.
        
         | larusso wrote:
         | 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.
        
         | YorickPeterse wrote:
         | I suspect you're referring to this article, which is a good
         | read indeed: https://mmapped.blog/posts/12-rust-error-handling
        
         | WhyNotHugo wrote:
         | > 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.
        
       | cute_boi wrote:
       | Rust should seriously stop using macros for everything.
        
         | kaashif wrote:
         | I agree with this. It appears that in this case, macros are a
         | band-aid over missing features in the type system.
         | 
         | I feel like structural typing or anonymous sum types would
         | solve this problem without macros.
         | 
         | I mean given A = X | Y and B = X | Y | Z, surely the compiler
         | can tell that every A is a B?
        
           | estebank wrote:
           | This has problems as soon as it hits generics and lifetimes.
           | 
           | That being said, I think a limited version of the feature
           | that disallows the the same type with divergent lifetimes or
           | even all generics would still be useful and well liked.
        
       | Animats wrote:
       | 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.
        
       | pjmlp wrote:
       | 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 wrote:
         | 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
        
           | johnisgood wrote:
           | 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.
        
           | jenadine wrote:
           | I prefer `derive_more` than thiserror. Because it is a
           | superset and has more useful derive I use.
           | 
           | color-eyre is better than anyhow.
        
       | drewlesueur wrote:
       | Is it just me or is the margin/padding altered. I notice this
       | article (being first) is squished up against the orange header
       | bar
        
       | IshKebab wrote:
       | Kind of reminds me of Java checked exceptions.
        
       | metaltyphoon wrote:
       | > 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.
        
       | the__alchemist wrote:
       | 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.
        
       | stephenlf wrote:
       | I apologize for this sidebar. I don't have much to contribute to
       | the technical content. It was an interesting read.
       | 
       | You use, too many, commas, in your, writing. It's okay to have a
       | long sentence with multiple phrases.
       | 
       | Thanks for sharing your thoughts.
        
       | xixixao wrote:
       | 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).
        
         | xixixao wrote:
         | 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).
        
         | estebank wrote:
         | 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).
        
       | kshri24 wrote:
       | > 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 :)
        
       | forrestthewoods wrote:
       | I quite like the Rust approach of Result and Option. The anyhow
       | and thiserror crate are pretty good. But yeah I constantly get
       | confused by when errors can and can not coerce. It's confusing
       | and surprising and I still run into random situations I can't
       | make heads or tails from.
       | 
       | I don't know what the solution is. And Rust is definitely a lot
       | better than C++ or Go. But it also hasn't hit the secret sauce
       | final solution imho.
        
       ___________________________________________________________________
       (page generated 2025-06-29 23:00 UTC)