ekimekim 2 hours ago

Ok, so the original idea of Result<T, Error> was that you have to consider and handle the error at each place.

But then people realised that 99% of the time you just want to handle the error by passing it upwards, and so ? was invented.

But then people realised that this loses context of where the error occured, so now we're inventing call stacks.

So it seems that what people actually want is errors that by default get transferred to their caller and by default show the call stack where they occured. And we have a name for that...exceptions.

It seems that what we're converging towards is really not all that different from checked exceptions, just where the error type is an enum of possible errors (which can be non-exhaustive) instead of a list of possible exception types (which IIUC was the main problem with java's checked exceptions).

  • tux3 2 hours ago

    It does seem to be converging somewhere, but a major difference that I really like is pushing humans a little more to care about errors, instead of just letting whatever bubble up from wherever until a catch(...) somewhere.

    With checked exceptions, it's very common for the user to end up with only a cryptic message from a leaf function deep inside something, and that's very hard to interpret.

    Having a manual stack of meaningful messages that add context is so nice as a user. Even if I do get the stacktrace in a program that threw a deep exception, you typically won't understand anything as a user without access to the code, the stack trace for exceptions is just not meant for human consumption.

    • shepmaster an hour ago

      > pushing humans a little more to care about errors

      This is 100% a reason that I like using SNAFU. The term I use for this is a "semantic stack trace" — a lot of the time, the person experiencing the error doesn't care that it occurred in "foo.rs" or "fn bar()" or "line 123". Instead, they care what the program is trying to do ("open the configuration file", "download the update file").

      When I'm putting effort into my errors, I basically never use `snafu::Location` or `snafu::Backtrace`. My error stacks should always be unique — any stack can exactly point to a trace through my program.

  • IshKebab 14 minutes ago

    > So it seems that what people actually want is errors that by default get transferred to their caller and by default show the call stack where they occured. And we have a name for that...exceptions.

    You've drawn the wrong conclusion - we don't want that by default. We want to chose. In most cases we'll just return the error to the caller, but we don't want it to be the default so we can miss critical points where we didn't want to do that.

  • PittleyDunkin 28 minutes ago

    > But then people realised that 99% of the time you just want to handle the error by passing it upwards

    This seems like a gross exaggeration

    > So it seems that what people actually want is errors that by default get transferred to their caller

    Hell no

  • kibwen 2 hours ago

    > show the call stack where they occured. And we have a name for that...exceptions.

    Getting a stack trace isn't a distinguishing feature of exceptions; stack traces predate the notion of exceptions. The distinguishing feature of exceptions is that they're a parallel return path all the way back up to `main` that you can ignore if you don't care to handle the error, or intercept at any level if you do. For some contexts I think this is fine (scripting languages), and for other contexts I think that being forced to acknowledge errors in the main return path is preferable.

    • danenania an hour ago

      I think a lot of it is psychological. Being forced to ask yourself "what do I want to happen if there's an error here?" every single time seems to go a very long way. If the answer is "ignore it" or "bubble it up" then fine, but at least you considered and explicitly answered that question rather than totally forgetting that an unhappy path exists. Default consider vs. default ignore.

  • jgilias an hour ago

    Yes and no. When a language has exceptions the code is perpetually wrapped in a fallible computational context. When the Result is reified as a type, you have the option (ha!) to write code that the type system guarantees won’t fail. This is nice.

    Let’s not talk about panics, shall we?

  • anon-3988 32 minutes ago

    I have a theory that what people actually want is something ala named exceptions + forced try catch with pattern matching + automaitally derived return Type.

  • zokier an hour ago

    That's not particularly novel observation; people have been pointing out the equivalence between checked exceptions and Result types for pretty much forever. See for example this thread from decade ago: https://news.ycombinator.com/item?id=9545647

exDM69 6 hours ago

Why does adding `backtrace` to thiserror/anyhow require adding debug symbols?

You'll certainly need it if you want to have human readable source code locations, but doesn't it work with addresses only? Can't you split off the debug symbols and then use `addr2line` to resolve source code locations when you get error messages from end users running release builds?

  • pornel 3 hours ago

    It should be possible (it'd need to also save memory map), but for some reason Rust's standard library wants to resolve human-readable paths at runtime.

    Additionally, Rust has absurdly overly precise debug info.

    Even set to minimum detail, it's still huge, and still keeps all of the layers of those "zero-cost" abstractions that were removed from the executable, so every `for` loop and every arithmetic operation has layers upon layers of debug junk.

    External debug info is also more fragile. It's chronically broken on macOS (Rust doesn't test it with Apple's tools). On Linux, it often needs to use GNU debuginfo and be placed in system-wide directories to work reliably.

    • exDM69 2 hours ago

      > (it'd need to also save memory map

      Typically the memory map is only required when capturing the backtrace and when outputting the stack frames' addresses relative the the binary file sections are given/stored/printed (with the load time address subtracted). E.g. SysRq+l on Linux. This occurs at runtime so saving the memory map is not necessary in addition to the relative addresses.

      Not sure if this is viable on all the platforms that Rust supports.

      > but for some reason Rust's standard library wants to resolve human-readable paths at runtime.

      Ah, I see that Rust's `std::backtrace::Backtrace` is missing any API to extract address information and it does not print the address infos either. Even with the `backtrace_frames` feature you only get a list of frames but no useful info can be extracted.

      Hopefully this gets improved soon.

      > External debug info is also more fragile.

      I use external debug info all the time because uploading binaries with debug symbols to the (embedded) devices I run the code on is prohibitively expensive. It needs some extra steps in debugging but in general it seems to work reliably at least on the platforms I work with. The debugger client runs on my local computer with the debug symbols on disk and the code runs under a remote debugger on the device.

      I'm sure there are flaky platforms that are not as reliable.

  • delusional 3 hours ago

    Your binary usually won't get loaded at the same address in memory. The addresses would be useless without the memory map.

    That's solvable though. The bigger problem is how you unwind the stack. the stack is not generally unwindable, unless you're the compiler. Debug symbols include information from the compiler about the stack sizes and shapes to help backtrace with unwinding the stack. It's quite possible to include such symbols in the final binary without adding debug symbols, a lot of compilers just don't have a specification for that.

    • exDM69 2 hours ago

      > Your binary usually won't get loaded at the same address in memory.

      The addresses you typically see in a backtrace error message (with debug syms disabled) are relative to the sections in the binary file, the runtime address it was loaded at has already been taken into account and subtracted. At least that's how you typically see a backtrace address in a typical native app on Linux.

      > The bigger problem is how you unwind the stack.

      Rust can unwind the stack on panic when built without debug symbols.

namjh 11 hours ago

> Consequently, this also means you cannot define two error variants from the same source type. Considering you are performing some I/O operations, you won't know whether an error is generated in the write path or the read path. This is also an important reason we don't use thiserror: the context is blurred in type.

This is true only if you add #[from] attribute to a variant. Implementing std::convert::From is completely optional. Personally I don't prefer it too as it ambiguates the context. I only use it for "trivially" wrapped errors like eyre::Report.

  • skavi 7 hours ago

    Yup. I absolutely would throw `#[from]` on everything when I started using thiserror, but now only do so in incredibly obvious cases like

      enum CarWontMove {
          EngineTroubles(EngineTroubles),
          WheelsFellOff(WheelsFellOff),
      }
    
    Even then, there’s often some additional context you can affix at that higher level.
lilyball 6 hours ago

> Then, to be able to translate the stack pointer we will need to include a large debuginfo in our binary. In GreptimeDB, this means increasing the binary size by >700MB (4x compared to 170MB without debuginfo).

Surely that's comparing full debuginfo, right? Backtraces just need symbols, not full debuginfo, and there's no way the symbols are 4x the size of the binary.

  • dwattttt 6 hours ago

    There's also split-debuginfo, which allows emission of debug info into a separate file, rather than needing to distribute it in the binary. Then they could capture stack traces, and resolve the symbols later if necessary. That would also address their concern about how long it takes to capture a stack trace, because just gathering the addresses themselves is quick.

DavidWilkinson 2 hours ago

Interesting approach! We had a similar journey at HASH to figuring out how we deal with stacked errors (as well as collecting parallel errors), developed the `error-stack` crate to solve for it. It works by abstracting over the boilerplate needed to stack errors by wrapping errors in a `Report`. Each time you change the context (which is equivalent to wrapping an error) the location is saved as well, with optional spantrace and backtrace support. It also supports supplying additional attachments, to enrich errors. We spent quite a bit of time on the user output, as well (both for `Debug` and `Display`) so hopefully the results are somewhat pleasant to work with and read.

shepmaster 11 hours ago

Hey all, I’m the author of SNAFU (mentioned in the article). I’m off to bed now, but I’d be happy to try and answer any questions people might have sometime tomorrow.

I’m glad to see SNAFU was useful to others!

  • zamalek 5 hours ago

    Its looks really neat! Two questions:

    * can it be used as a build dependency (i.e symbols from the snafu crate don't appear in the generated code).

    * I assume you have to use one of the macros (ensure! or location!) when constructing an error that contains a location?

    • shepmaster an hour ago

      It can't be used as a literal build dependency [0], no. However, the fact that your crates uses SNAFU should [1] be completely hidden from your users. From the outside, you just return a regular enum or struct as your error type. If you were to look at the symbols in the resulting binary, I would expect that you could see references to the trait method `snafu::ResultExt::context` (and similar functions across similar types) depending on how well the code was inlined. If you use other features like `snafu::Location` or `snafu::Report`, those would definitely show up.

      You don't have to use the macros, no. When you define your error type, you can mark a field as `#[snafu(implicit)]` [2]. When the error is generated, that field will be implicitly generated via a trait method. The two types this is available for are backtraces and locations, but you could create your own implementations such as grabbing the current timestamp or a HTTP request ID.

      [0]: https://doc.rust-lang.org/cargo/reference/specifying-depende...

      [1]: There's one tiny leak I'm aware of, which is that your error type will implement the `snafu::ErrorCompat` trait, which is just a light polyfill for some features not present on the standard library's `Error` trait. It's a slow-burn goal to remove this at some point, likely when the error "provider API" stabilizes.

      [2]: https://docs.rs/snafu/latest/snafu/derive.Snafu.html#control...

Sytten 10 hours ago

What is really annoying with thiserror is the wizard refusal to give us an easy way to print the error chain. No I dont want to convert it to anyhow just to print the error...

  • lumost 10 hours ago

    Rust is full of these, I’ve found the community simply falls back on user error to understand rust when vexed by in my opinion basic software operations.

    As someone who works extensively in cpp/java/python. I want so much to love rust, but unfortunately I haven’t found it to be productive after 6+ side projects.

    • nixpulvis 10 hours ago

      Rust's community is slightly more fragmented than it should be. The community being built while the language was changing so dramatically (e.g. async) didn't help, but it also is part of what lead to Rust in the first place.

      But it's still somewhat young, lots of stuff is being built. So some of the lack of productivity probably just comes from not knowing the right stacks yet.

joshka 6 hours ago

It's technically feasible to add SpanTrace support to thiserror fairly easily (30 mins work - Issue: https://github.com/dtolnay/thiserror/issues/400, PR: https://github.com/dtolnay/thiserror/pull/401). This would solve part of the problem in a way that is meaningfully good for that side of the ecosystem. I suspect you could probably do something similar for Snafu

  • shepmaster 38 minutes ago

    Without deeply looking into it, I'd expect that to integrate with SNAFU, you could basically write something like this:

        struct SpanTraceWrapper(tracing_error::SpanTrace);
        
        impl snafu::GenerateImplicitData for SpanTraceWrapper {
            fn generate() -> Self {
                Self(tracing_error::SpanTrace::capture())
            }
        }
    
    And then you can use it as

        #[derive(Debug, Snafu)]
        struct SomeError {
            #[snafu(implicit)]
            span_trace: SpanTraceWrapper,
        }
    
    This will capture the `SpanTrace` whenever `SomeError` is constructed (e.g. `thing().context(SomeSnafu)` or `SomeSnafu.fail()`.
gfreezy 11 hours ago

    async fn handle_request(req: Request) -> Result<Output> {
        let msg = decode_msg(&req.msg).context(DecodeMessage)?; // propagate error with new stack and context
        verify_msg(&msg)?; // pass error to the caller directly
        process_msg(msg).await? // pass error to the caller directly
    }

    async fn decode_msg(msg: &RawMessage) -> Result<Message> {
        serde_json::from_slice(&msg).context(SerdeJson) // propagate error with new stack and context
    }

how to capture the virtual stack when `verify_msg` returns an error? Do you have some lint to make sure every error is attached with a context?
  • shepmaster 31 minutes ago

    I don't think you need a lint. When you define the error type returned by `handle_request`, you decide how the error type returned by `handle_request` will be incorporated. If you've decided to implement `From` then you've decided you don't want/need to add context. Otherwise, the compiler will give you an error when you use `?`.

    The time I can think this won't work is when you are reusing error types across places. Recently, I've been experimenting with creating a lot of error types, so far as one unique error type per function. I haven't done this for long enough to have a real report, but I haven't hated it so far.

samanthasu 13 hours ago

A good error report is not only about how it gets constructed, but what is more important, to tell what human can understand from its cause and trace. In this example, we analyzed and showed how to design stacked errors and what should be considered in this process.

dgfitz 11 hours ago

Rust is such a powerful yet completely disgusting language. I’m teaching myself rust out of spite at this point.

Everything about the syntax of the language is awful.

  • GrantMoyer 10 hours ago

    This seems to be a fairly common sentiment. I consider Rust's syntax fairly consistent and elegant for a curly brace language, but evidently I have some blind spots. What quibbles do you have with Rust's syntax?

    • akira2501 10 hours ago

      The explosion of single character sigils and the taint of C++'s template syntax.

      • maxk42 8 hours ago

        The only single-character sigils I can think of are '&', '*', '\'', and maybe '?'. Am I missing any?

        • akira2501 6 hours ago

          I kinda feel like macros!() should count.

        • troupo 7 hours ago

          It's probably the explosion of those characters and punctuation characters like in this example: https://x.com/AndersonAndrue/status/1864457598629540348

          To quote Tsoding, https://x.com/tsoding/status/1832631084888080750: "Rust is a very safe, unergonomic language with annoying community and atrocious syntax. Which is somehow surprisingly miles better than modern C++."

          • saghm 4 hours ago

            > It's probably the explosion of those characters and punctuation characters like in this example: https://x.com/AndersonAndrue/status/1864457598629540348

            I feel like there's plenty of places to make criticisms of Rust's syntax, but the example they picked has like half a dozen places where the full path to reference an item is used instead of importing it. Sure, there are languages where you're required to import things rather than referencing them in the full path, but there are also languages where you don't have any flexibility in the paths to use something (e.g. Go and Java) or where they dump literally everything into a single namespace with no context (e.g. C and C++). Using the entire path to something instead of importing it is absurdly less common by at least an order of magnitude over importing things directly, so it's not like people are abusing it all over the place (if anything, people probably import things _more_ than they need to, like with top-level functions that might otherwise make their provenance obvious). Having an option to do things verbosely that most people don't actually do is "unergonomic"? It's like saying the problem with Unix file paths is that using absolute paths for literally everything is ugly; sure, it can be, but that's also just not how 99% of people use them.

          • umanwizard 2 hours ago

            The first example doesn’t show anything particularly wrong with Rust syntax. You can construct a troll example like this of an arbitrarily complex type in any language with generics.

            The second tweet, as is common for these discussions, doesn’t give any specific example of what’s wrong with Rust syntax.

            I remain baffled by how many people hate Rust syntax. I really don’t get it!

          • FridgeSeal 2 hours ago

            Why is old mate artificially constructing the most non-representative example possible?

            Full import paths for map and iter? I’ve never seen anyone double-underscore stuff in Rust. Outside of specific stdlib cases, I haven’t seen anyone manually need to (or do) specify iterator adaptors like that.

            They could have at least chosen a representative example. This just comes across as fabricating something to get angry at.

          • swiftcoder 5 hours ago

            Anderson's example has literally one more sigil than the equivalent C++

      • Vecr 10 hours ago

        What's mandated to be a single character? I'm not sure what the popular style is today.

      • littlestymaar 6 hours ago

        > and the taint of C++'s template syntax.

        Interestingly enough, nobody says that when talking about TypeScript…

        • galangalalgol 2 hours ago

          What languages have better template syntax?

  • jtrueb 11 hours ago

    Which statically typed language do you find most agreeable?

    Same question for any language.

  • speed_spread 11 hours ago

    Rust's syntax is largely irrelevant to its purpose. If you don't see the need for it, might as well learn something else.

    • more-nitor 9 hours ago

      idk if its about a few syntax, then it's possible to make a temp proc-macro for those