Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

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.



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


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


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.


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.


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.


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


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.


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


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


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.


> 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)?
    }




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: