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.
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.
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).
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.
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.
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.
https://home.expurple.me/posts/rust-solves-the-issues-with-e...
You're just providing more examples of my argument that new language features are not made with checked exceptions in mind.
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?
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.
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)'
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.
> (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...)
> 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
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.
Your application gets a new context and you add another field? There must be a better way to do this.
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.
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)?
}I agree it's an issue, and I hope the community centers around better practices.
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.
In general, if you're making serious software, you shouldn't assume source knowledge at all in user facing messaging.
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.
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.
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.)
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.
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`. 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 functionshttps://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`.
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.