arccy
6 days 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
6 days 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
5 days 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
5 days 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.
tough
5 days ago
anyhow and thisiserror are my rust error lib defaults nowadays
Mawr
3 days ago
Yeah, so that's not how anything humans do works. Languages are designed for humans, so you must analyze from the perspective of humans. You can't merely compare "functional differences" and call it a day.
Watch this until the end: https://youtu.be/MmPtfy9P714?t=101
So now, what's more likely, that someone is just going to slap a "?" at the end of a line and be done with it or that they're going to, unprompted, research error handling libraries, choose and install one, then go out of their way to add context to each line?
sfn42
5 days 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
5 days 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 :/
Expurple
4 days ago
Hi, I'm the author of the article. As I've learned when discussing my older article about exceptions, the real problem is that methods can't have an unknown, generic, composable set of checked exceptions!
https://home.expurple.me/posts/rust-solves-the-issues-with-e...
lesuorac
4 days ago
Generics were added later than checked exceptions.
You're just providing more examples of my argument that new language features are not made with checked exceptions in mind.
ninkendo
6 days 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.
arccy
6 days 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
tough
5 days ago
go has a really good ecosysstem between wrapcheck, gocritic, and golint
Arnavion
6 days 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
5 days 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
5 days 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.
more-nitor
6 days 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)'
Expurple
4 days ago
Could you elaborate on your first idea? I don't understand it.
Your second idea is interesting, but I feel like it would be too magical for Rust. You fundamentally need a Backtrace field to add backtraces to your error types. Adding an invisible, inaccessible, always-implicitly-initialized field sounds too weird and non-Rusty to me. The language doesn't have anything like that right now.
Also, which types should even get this special field? Every type that implements the Error trait? Again, it's a very weird special case full of magic.
more-nitor
4 days ago
there's ("I'm hoping someone would...") phrase omitted here...
> (I'm hoping someone would) create `with_err` function: some_mayerr_func().with_err_log()
> (I'm hoping someone would) add 'newbie beginner mode(default)' / 'expert mode (optimized)' for rust
about: Adding an invisible, inaccessible, always-implicitly-initialized field sounds too weird and non-Rusty to me
maybe rust-devs are too focused on "zero-cost abstraction"? I mean, if I'm building for embedded sys, I have to make sure I don't waste any ram... but for other cases like web-server dev, it's much better to have some-cost abstraction that helps debugging
(this might actually be one of the reason rust isn't used a lot despite having an extremely good language basis/growth -- other one being async cancellation...)
Expurple
3 days ago
To me, it's less about memory usage and performance, and more about language simplicity and constistency (which is another big advantage of Rust, although many people would disagree with me on this).
> this might actually be one of the reason rust isn't used a lot despite having an extremely good language basis/growth
I think, it's mostly these two things:
1. Rust is not suited for a certain exploratory style of programming that many people prefer. It's more suited for writing robust production systems and infrastructure software (which together can still be a large niche).
2. But the ecosystem isn't quite there yet, to justify using it in production over the alternatives. I guess, unless the alternatives are C/C++ and their existing libraries aren't crucial to the domain :D
nine_k
3 days ago
If only there'd be an automatic handler that was invoked every time `?` hits a None, and transformed the error to a richer one, using the context of the entire function. Something like... and exception handler :)
Java and Python use stacks of exception handlers that catch and re-throw exceptions, chaining them and adorning with contextual information. It works very well: you get a stack trace, and a number of human-oriented messages with relevant context.
Unfortunately, the whole idea of walking up the stack and looking for an exception handler crumbles down once you touch async calls, as evidenced by JS/TS and C#'s LINQ. It's almost as if you want to pass a context object to any fallible function, to be logged if an error happens. It's very similar to passing other context, like the `this` pointer in OOP, or implicits in Scala (uhg!). Lifetime of such an object is also going to be an interesting problem, because you likely don't want to copy it all the time, so it must be mutable.
Expurple
2 days ago
> If only there'd be an automatic handler that was invoked every time `?` hits a None, and transformed the error to a richer one, using the context of the entire function.
Not sure what you mean by "using the context of the entire function". To me, this "automatic enrichment" sounds like adding a backtrace. If you use `anyhow`, your errors automatically get backtraces. And if you want to preserve error type information, you can easily write your own wrapper:
use std::backtrace::Backtrace;
use std::error::Error;
use std::fmt::{Debug, Display};
use std::num::ParseIntError;
#[derive(Debug)]
struct WithBacktrace<E> {
source: E,
backtrace: Backtrace,
}
impl<E> Display for WithBacktrace<E>
where
E: Display,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.source.fmt(f)
}
}
impl<E> Error for WithBacktrace<E>
where
E: Error + 'static,
{
fn source(&self) -> Option<&(dyn Error + 'static)> {
Some(&self.source)
}
}
impl<E> From<E> for WithBacktrace<E> {
fn from(source: E) -> Self {
Self {
source,
backtrace: Backtrace::capture(),
}
}
}
If you use this type in the function signature, `?` will add backtraces automatically: fn add_a_backtrace_to_another_error() -> Result<(), WithBacktrace<ParseIntError>> {
let num = "not_a_number".parse::<i64>()?;
Ok(())
}
You can do something similar on `None` too. Although, you can't directly `?` on `None` in a function that returns `Result`: #[derive(Debug, thiserror::Error)]
#[error("encountered a `None` value")]
struct NoneError;
fn backtrace_on_none() -> Result<(), WithBacktrace<NoneError>> {
None.ok_or(NoneError)?
}
nixpulvis
6 days 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
6 days 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
6 days 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
6 days ago
getting rid of ? might help...
nixpulvis
6 days 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
6 days 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
6 days 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
6 days 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
6 days 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
5 days 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.)
RealityVoid
5 days ago
I did not claim anything about "better" - I just said that if you don't want to deal with errors, you unwrap it - it's the go-to behavior if you just want to make it build. That is what the language makes "much easier than the alternative". If you want to make it robust, you can go through the unwraps and handle them with the care they deserve.
I agree about needing to have care about it, but I was fighting the claim that the poor error handling is an issue with the language.
user
6 days ago
jgraettinger1
5 days 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
5 days 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 functionsuser
5 days ago
timhh
5 days 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
6 days 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.