Why Use Structured Errors in Rust Applications?

72 todsacerdoti 53 6/1/2025, 4:48:19 AM home.expurple.me ↗

Comments (53)

arccy · 21h ago
Having had to work with various application written in rust... i find they have some of the most terrible of errors. "<some low level operation> failed" with absolutely no context on why the operation was invoked in the first place, or with what arguments.

This is arguably worse than crashing with a stack trace (at least i can see a call path) or go's typical chain of human annotated error chains.

jeroenhd · 18h ago
There's no functional difference between Go and Rust here, other than that the Rust code you seem to interact with doesn't seem to bother wrapping errors as often. Neither languages have very good errors. Then again, no language I know does errors well; I'd like Java's error system if Java developers didn't insist on making every exception a runtime exception you can't know you need to handle unless you read the source code.

FWIW, you may be able to see the stack traces by enabling them in an environment variable (export RUST_BACKTRACE=1). By default, errors are dumped without context, but enabling back traces may show stack traces as well as detailed human-written context (if such crates are in use).

tsimionescu · 14h ago
There is a difference between Go and Rust on the error ha sling front.

In Go, the error handling is forced to be so verbose that adding a little bit of context doesn't significantly increase code size/effort, so programmers tend to actually add that context. Also, there is a very simple function for adding context to errors in the stdlib itself, fmt.Errorf.

In contrast, the difference in Rust between just returning an error (a ? at the end of the expression that can return one) vs adding context is much higher. Especially so since the standard library doesn't offer any kind of error context, so you either have to roll your own or use some error handling crate (apparently anyhow is very popular).

So, while at the language level there is little difference, the ecosystem and "incentives" are quite different.

dllthomas · 14h ago
> the difference in Rust between just returning an error (a ? at the end of the expression that can return one) vs adding context is much higher

I don't disagree with what you've said, but I think this is likely to be read too strongly.

Adding a map_err before the ? is only a big step up in verbosity because the ? alone is so very terse.

lesuorac · 11h ago
I just always make my own error type and as part of their creation throw a backtrace into the struct. Then using `?` gives me a stack trace for free.
dllthomas · 8h ago
Yeah, if all the context you want is the backtrace, that works just fine.
tough · 13h ago
anyhow and thisiserror are my rust error lib defaults nowadays
sfn42 · 14h ago
I like Java's exceptions. I've been working in C# and i really miss checked exceptions.

Feels like if you use them well they're a great tool, but sadly a lot of devs just don't use the available tools very well.

lesuorac · 11h ago
The problem with checked exceptions in Java is that pretty much every language feature added since then has assumed unchecked exceptions (use an IO operation in a stream). So they start to stick out in a lot of places :/
ninkendo · 19h ago
Yeah, sometimes I think the `?` early-return operator is an anti-pattern. It’s too easy to reach for, and results in these sort of context-less “Operation failed” messages.

I wish there were a better way to enforce that you can’t use the early return syntax without adding context to the error in some structured way (so that the result is something resembling a stack trace.) Maybe a particular WrapsError trait that the compiler knows about, which constructs your error with the wrapped error and callsite information, so that you can annotate it properly, and is automatically used for `?` early-returns if the return type supports it.

At some point it does feel like we’re just reimplementing exceptions poorly though.

> or go's typical chain of human annotated error chains

It’s interesting you say this, because go has basically the exact same analogous problem: People can just do `if err != nil { return nil, err }`, and it results in the same problem. And the last time I did much go programming I remember having a very similar complaint to yours about Rust. Maybe more go programmers have begun to use annotations on errors since then? There’s nothing really different about rust here, anyhow/etc make it just as easy to annotate your errors.

Arnavion · 15h ago
If you don't `impl From<UnderlyingError> for MyError` then you can force the code to do `.map_err(|err| MyError::new(err, context))?` instead of just `?`.

This doesn't work when the error is already MyError so you want to make sure that `context` contains everything you might want to know about the call stack. Eg if you're writing a web server application you might have an error at the network layer, the HTTP layer, the application layer or the DB layer. So your context can have four Option fields and each layer can populate its field with whatever context makes sense for it.

fn-mote · 14h ago
Four optional fields one for each potential context really strikes me as a code-smell / anti-pattern.

Your application gets a new context and you add another field? There must be a better way to do this.

Arnavion · 13h ago
The better way is to do what I wrote in the first paragraph; ie each layer gets its own error type with a bespoke context type. The second paragraph is about the case where you want to use a single error type.
arccy · 18h ago
maybe because lots of go enterprise software development turns on a bunch of linters, which will include wrapcheck that complains if you don't wrap

https://github.com/tomarrell/wrapcheck

tough · 13h ago
go has a really good ecosysstem between wrapcheck, gocritic, and golint
more-nitor · 17h ago
hmm this might be solved in some simple ways:

1. create `with_err` function: some_mayerr_func().with_err_log()?

2. add 'newbie beginner mode(default)' / 'expert mode (optimized)' for rust:

- current Rust default is a bit too focused on code-optimization, too much so that it defaults to removing backtrace

- default should be 'show every backtrace even for handled errors (in my code, not the lib code -- bonus points for "is handled" checkbox)'

nixpulvis · 21h ago
I don't think this is really a failing of the language, just a bad practice in assuming that you can bubble up a low level error without adding some more context to why it was trigged in this case. Short of matching the error and returning a new error with more info, I'm not sure what the idiomatic solution is personally. But I also haven't spent as much time as I should with error crates.

I agree it's an issue, and I hope the community centers around better practices.

arccy · 20h ago
I think it is a language issue, if the language makes a bare return much easier than the alternative, then that's what people will be inclined to do.
nixpulvis · 19h ago
When I say it's not a language issue, that's because I can imagine a crate or design pattern filling this role, and I cannot as easily imagine a change to the language itself that would make it better. But I also haven't given this as much thought as it deserves.
arccy · 18h ago
getting rid of ? might help...
nixpulvis · 18h ago
I don't think that helps, there are a lot of intermediate steps that rightfully propagate or union errors and `?` makes that a lot easier.

The issue is figuring out when to not do that and wrap a low level error with a higher level error without losing the critical information and making things to generic.

RealityVoid · 19h ago
Easiest error handling is just .unwrap(). It's true you can just leave it messy like that, but it at least leaves you an obvious trail where you can go and clean up the error reaction.
nixpulvis · 18h ago
Easier yes; better absolutely not. A user should not be given an unwrap error and expected to read the code to figure out what went wrong.

In general, if you're making serious software, you shouldn't assume source knowledge at all in user facing messaging.

MangoToupe · 18h ago
This seems incredibly bad faith—using "unwrap" is closer to using an assert than it is at being "user facing messaging". Asserts don't typically come with manuals explaining what the user should do to fix the software.

We all have moments when we're developing software and haven't fully developed the domain. Not requiring exhaustive error handling during this process is a massive help. Obviously, fully enumerated errors are good, and catering diagnostics to the users is even better. But there's more to software than shipping perfect code.

nixpulvis · 15h ago
I do not understand your point. Are you saying more code should use asserts for catching error cases?

Nobody's saying perfection is 100% required, but there are ideals and we should aim for them and design with principle. One of those principals should be quality error messages.

Hell, the rust compiler itself is a great case study for how improving error messages for users massively helps with adoption and quality of life. It's pretty rare that you hit an internal compiler error (ICE) too which is nice.

MangoToupe · 12h ago
I'm certainly not against high-quality diagnostics, I'm just saying that few people expect an unwrap to be sufficient for this, and it's odd to me to look at an unwrap and see it as error messaging (outside of what an assert is supposed to provide—eg a clear inconsistency in logic a developer needs to interpret). But to figure out which errors you need to appropriately parameterize and pass up the stack, you first need to understand what you're building. Without unwrap, we would basically have to engage in exhaustive enumeration as we develop. Which is fine, if you're willing to put that effort in! But that's part of why things like Haskell can struggle to win people over—for better or worse, half-broken code (semantically, in domain terms) that compiles and runs is a powerful tool mid-feature.

But yes, this does require that people care about diagnostics to begin with. I don't think that's worth forcing; most of us develop this instinct through frustration at dealing with the fallout of someone not caring.

(And FWIW I find the combination of explicitly-structured Results that are manually passed up the stack with RAII to be a very, very sweet spot for development velocity, and also easy to teach. I very much prefer this to both exceptions and go's approach.)

jgraettinger1 · 15h ago
I would recommend the `anyhow` crate and use of anyhow::Context to annotate errors on the return path within applications, like:

  falliable_func().context("failed to frob the peanut")?
Combine that with the `thiserror` crate for implementing errors within a library context. `thiserror` makes it easy to implement structured errors which embed other errors, and plays well with `anyhow`.
kaathewise · 14h ago
Yeah, I found `anyhow`'s `Contex` to be a great way of annotating bubbled up errors. The only problem is that using the lazy `with_context` can get somewhat unwieldy. For all the grief people give to Go's `if err != nil` Rust's method chaining can get out of hand too. One particular offender I wrote:

   match operator.propose(py).with_context(|| {
    anyhow!(
   "Operator {} failed while generating a proposal",
   operator.repr(py).unwrap()
  )
   })? {
Which is a combination of `rustfmt` giving up on long lines and also not formatting macros as well as functions
timhh · 12h ago
Yeah I agree - in fact I ran into this very issue only hours ago. The entire error message was literally "operation not supported on this platform". Yeay.

https://github.com/rust-lang/rust/issues/141854

> I'll also note that this was quite annoying to debug since the error message in its entirety was `operation not supported on this platform`, you can't use RUST_BACKTRACE, rustfmt doesn't have extensive logging, and I don't fancy setting up WASM debugging. I resorted to tedious printf debugging. (Side rant: for all the emphasis Rust puts on compiler error messages its runtime error messages are usually quite terrible!)

Even with working debugging it's hard to find the error since you can't set a breakpoint on `Err()` like you can with `throw`.

nulld3v · 16h ago
I don't think it's worse than a stacktrace. If the app is bubbling up low-level errors like that, it is almost certainly a "lazy" error handling library like anyhow/eyre which allow you to get a stacktrace/backtrace if you run the app with: `RUST_BACKTRACE=full`.

The ecosystem should probably enable backtraces by default though, for all GUI apps at a minimum, the overhead it adds is almost always worth it. It's not good practice to use "lazy" error handling in performance-oriented code anyways, it will generate tons of heap allocations and uses dynamic-dispatch.

lifthrasiir · 20h ago
I recently saw the `error_mancer` crate [1] that automatically generates error types for each function so that error definitions and functions are adjacent to each other. While it wouldn't be sufficient for all cases, I quite liked its approach.

[1] https://crates.io/crates/error_mancer

dlahoda · 19h ago
nice approach, seems would be awesome for http openapi.
stevefan1999 · 6h ago
Try not to use thiserror but this instead: https://docs.rs/derive_more/latest/derive_more/

Reason: thiserror still requires std which greatly hinder its use for no_std context

junon · 5h ago
Thiserror only relies on std because primarily due to the use of the std Error type which just got stabilized into core not too long ago.
ozgrakkurt · 22h ago
You only ever need backtrace with line numbers and error kind enum for library code and app code that is no supposed to fail.

For app code that is supposed to fail, you need full custom error setup.

For example in a a http server you want to cleanly convert error to http response and a record on server side for viewing later.

imo zig implements errors in language pefectly

alpaca128 · 20h ago
I don't get what makes Zig errors better. As I understand it they're just named error codes with special syntax for handling them, meaning any more complex error type or any kind of contextual information has to be dealt with separately. If you want to return the location of a syntax error in the input you can't provide that information in the error. Zig has a couple opinionated properties I find strange, but the error handling is the one I find the most puzzling.

Meanwhile Rust treats all possible error types the same way as long as they're wrapped in Result.

anonymoushn · 19h ago
I think that like actually using Rust and encountering situations where one must define a new error enum that exists only to wrap the values returned by two different fallible functions belonging to different libraries all the time was sufficient to convince me that the situation in Zig is preferable. It's also generally good that users are unable to attach strings to their errors when using the built-in error handling, in the sense that things that are usually bad ideas ought to be a bit more difficult, so that users do not end up doing them all the time.
kbolino · 15h ago
> one must define a new error enum that exists only to wrap the values returned by two different fallible functions belonging to different libraries

Saying this must be done is awfully strong here. There are other tools at your disposal, like dyn Error and the anyhow crate.

kartoffelsaft · 10h ago
It "must" be done in the sense that `dyn Error`, anyhow, and (if we include zig's equivalent) inferred error sets mean something different than setting up an error enum. Using an error enum makes the failures explicit, which (most) other options choose to discard because they're designed to handle any error.

Your options effectively become either writing that enum, or using a macro that reduces to that enum. Unless, of course, you're willing to have the function signature say "eh, this function just breaks sometimes. Could be anything really."

littlestymaar · 17h ago
> in the sense that things that are usually bad ideas ought to be a bit more difficult,

I didn't know that the Zig folks had embraced the Go philosophy of “the language designer knows better than you” for trivial stuff like that.

That's the kind of behavior that lead people to build their own version of things, incompatible with the whole ecosystem, just to be free from the petty arbitrary limitation.

alpaca128 · 15h ago
They also settled the tabs vs. spaces debate by declaring tabs a syntax error, end of discussion. Same with unused variables which are a compile-time error.

The thing is, I don't disagree with the fundamental arguments that led to these decisions, I just think it goes a step too far and they're trying too hard. I understand they aim to reduce footguns in C with that language but that doesn't mean they have to prevent someone from wearing the wrong shoe for the occasion.

littlestymaar · 14h ago
> They also settled the tabs vs. spaces debate by declaring tabs a syntax error,

Did Zig really do that? Because even go didn't, it's just the default of gofmt and you can't override it, but not a hard error.

> Same with unused variables which are a compile-time error.

Which is a pain in the ass for anyone attempting to refactor or iterate on a go codebase. A warning is just the better option for this kind of things. That's the entire purpose of the concept of warning.

alpaca128 · 12h ago
https://github.com/ziglang/zig/issues/544#issuecomment-61807...

> In order to simplify everything, tabs are not allowed. Spaces are necessary; we can't ban spaces.

They seem to be open to the alternative but not to a solution that isn't forced on people:

> Maybe someday, we'll switch to tabs for indentation, spaces for alignment and make it a compile error if they are incorrectly mixed

Zig already has a builtin code formatter that automatically changes formatting to their preference, but that's not enough.

> That's the entire purpose of the concept of warning

Yes, but as far as I know the Zig compiler cannot emit warnings, only errors. Because warnings are potential footguns too and we can't have that.

const_cast · 11h ago
> Yes, but as far as I know the Zig compiler cannot emit warnings, only errors. Because warnings are potential footguns too and we can't have that.

Okay they're kind of on to something here, because in my experience with C, warnings just get ignored. And you only get warning if you're doing something really suspicious in the first place.

Every serious C project I've worked on has used -Werror.

alpaca128 · 10h ago
I 100% understand the motivation and agree with it. But an optional compiler flag would be nice for development, instead of the special syntax that has to be applied per unused identifier.

And I just don't get why the compiler has to throw an error about a formatting preference when the builtin formatter already changes it accordingly. Any serious project can just mandate use of the formatter to keep things consistent and be done with it. Instead the github issues have people debate tabs vs spaces because the compiler took a side in order to end the "bikeshed of all bikesheds", ironically contributing to it.

littlestymaar · 11h ago
> Every serious C project I've worked on has used -Werror.

-Werror is a good default for CI, but not for local development.

const_cast · 8h ago
Yeah I would agree, same with unused variables.
littlestymaar · 12h ago
Ugh.

Thanks nonetheless.

layer8 · 16h ago
“Not supposed to fail” can be context-dependent though, and/or depend on the specific error (code), so whether you need a stack trace or not may only be decidable after the fact.
nixpulvis · 21h ago
Can you elaborate on what Zig does that makes it better in your opinion?
reynoldsbd · 14h ago
I’ve found that good logging/tracing discipline can play an important role here. For example, my team has a guideline to put a debug! message before blocks of code that are likely to fail. Then if something goes wrong, we can dial up the logging level and zero in on the problem fairly efficiently. With this model, we don’t find the need to use any third party error framework at all.

In the abstract, we are basically treating the error as a “separate thing” from the context in which it occurred. Both are of course strongly related, however the way some of these error libraries try to squash the concepts together can be quite opinionated and doesn’t always facilitate smooth interop.

Not saying this is the only way to deal with errors, but it’s just one way of thinking of the problem that I’ve had pretty good success with.

suddenlybananas · 1d ago
I just wish it were simpler to set up backtraces with thiserror.
oldpersonintx2 · 17h ago
went down a similar path

`anyhow` is a very cool library but in the end it just felt a little ick to use, like I was being lazy or something