[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)