Skip to content(if available)orjump to list(if available)

Why Use Structured Errors in Rust Applications?

arccy

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

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

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

> 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.

tough

anyhow and thisiserror are my rust error lib defaults nowadays

sfn42

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

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

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.

arccy

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

go has a really good ecosysstem between wrapcheck, gocritic, and golint

Arnavion

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

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.

more-nitor

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

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

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

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.

RealityVoid

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.

null

[deleted]

jgraettinger1

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

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

null

[deleted]

timhh

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

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

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

nice approach, seems would be awesome for http openapi.

ozgrakkurt

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

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

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

> 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.

littlestymaar

> 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.

layer8

“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

Can you elaborate on what Zig does that makes it better in your opinion?

stevefan1999

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

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.

reynoldsbd

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

I just wish it were simpler to set up backtraces with thiserror.

oldpersonintx2

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