[HN Gopher] Error Stacking in Rust
       ___________________________________________________________________
        
       Error Stacking in Rust
        
       Author : samanthasu
       Score  : 121 points
       Date   : 2024-12-19 01:47 UTC (21 hours ago)
        
 (HTM) web link (greptime.com)
 (TXT) w3m dump (greptime.com)
        
       | samanthasu wrote:
       | 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.
        
       | gfreezy wrote:
       | 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 wrote:
         | 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.
        
       | namjh wrote:
       | > 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 wrote:
         | 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.
        
           | shepmaster wrote:
           | SNAFU follows much the same idea: we have an attribute you
           | can add [0] when you want to allow directly implementing
           | `From`. Like thiserror, you can also mark an error as
           | transparent [1] when even the error existing doesn't provide
           | useful information.
           | 
           | [0]: https://docs.rs/snafu/latest/snafu/derive.Snafu.html#dis
           | abli...
           | 
           | [1]: https://docs.rs/snafu/latest/snafu/derive.Snafu.html#del
           | egat...
        
       | shepmaster wrote:
       | 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 wrote:
         | 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 wrote:
           | 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#con
           | trol...
        
       | Sytten wrote:
       | 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 wrote:
         | 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 wrote:
           | 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.
        
             | lumost wrote:
             | It's young, but my experience has been that developer
             | ergonomics is not a focus, to the extent that c++ has a
             | much stronger devex story.
        
       | zote wrote:
       | URL changed, I think: https://greptime.com/blogs/2024-05-07-rust-
       | error-handling
        
       | joshka wrote:
       | 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 wrote:
         | 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()`.
        
           | joshka wrote:
           | Neat :)
        
       | lilyball wrote:
       | > _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 wrote:
         | 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.
        
       | exDM69 wrote:
       | 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?
        
         | delusional wrote:
         | 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 wrote:
           | > 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.
        
           | umanwizard wrote:
           | You don't need debug symbols to unwind the stack, you just
           | need the .eh_frame section, which compilers emit by default
           | regardless of whether you're building with debug symbols.
           | 
           | Source: I work on a profiler (Parca) that does stack
           | unwinding. It works fine on Rust binaries with or without
           | debug symbols.
        
         | pornel wrote:
         | 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 wrote:
           | > (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.
        
       | ekimekim wrote:
       | 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 wrote:
         | 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 wrote:
           | > 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.
        
             | jcelerier wrote:
             | But... It's not the user that is seeing this, it's the
             | developer. You catch at the top of your event loop and you
             | log the stack trace to some place that can be reached by
             | the dev team, be it Jira, some crash reporting tool, etc.
        
               | jandrese wrote:
               | Yeah, but lots of diagnostic work is done by end users in
               | the real world. Users rarely have good access to the
               | developer team, if the team even still exists. Usually
               | there are layers of insulation that mean your problem
               | might be looked at in a few weeks or months only if the
               | company thinks it might be interesting. Meanwhile you
               | have your problem to fix and it is off to stack traces
               | and access logs to try to figure out what went wrong.
               | Maybe some library updated. Maybe there was a permissions
               | change. Maybe some policy change at the OS level. Maybe
               | some external resource went away or changed syntax. It is
               | up to you as the end user to figure it out and fix it, or
               | at least figure out a unique enough error message that
               | you can Google to find someone else with the same
               | problem.
               | 
               | There is nothing more frustrating than a dialog box that
               | says "An error occurred" and then the program shuts down.
               | Frankly I'd rather it crashed hard, at least then I might
               | have some evidence to sift through in the blast zone.
        
               | Groxx wrote:
               | > _Yeah, but lots of diagnostic work is done by end users
               | in the real world. Users rarely have good access to the
               | developer team, if the team even still exists._
               | 
               | And hiding details prevents them from being able to know
               | if error X is different from error Y, yes.
               | 
               | It's an unhandled error at that point. You _do not know_
               | what is relevant, essentially by definition, because
               | otherwise you would have handled it.
               | 
               | Display messages are almost completely unrelated to error
               | handling, and have almost completely unrelated needs. If
               | you decide to combine them, I'm pretty convinced that
               | it's ALWAYS better to show ALL context somewhere, because
               | otherwise troubleshooting frequently becomes impossible.
               | It doesn't have to be a megabyte of stack trace info in a
               | dialog box shown all the time, save it to a file and link
               | to it or something.
        
             | gpderetta wrote:
             | The end user might not care, but as the developer I very
             | much care about having a line-accurate backtrace.
        
               | shepmaster wrote:
               | When presented with a bug from the field, I also care
               | about finding the path through my code where it occurred,
               | but rarely do I need to know that `foo` called
               | `foo_with_caching` called `foo_with_caching_recursive`.
               | When reading a backtrace, I skip over amounts of
               | "implementation details" to get a big picture. For me,
               | the exact functions / files / line numbers are not
               | relevant, doubly so if I'm working in a situation where
               | the error message isn't tied to a specific git commit and
               | the functions/files/lines have moved over time.
               | 
               | To reiterate my point from above though, my error stacks
               | are all unique -- seeing the stack will point me to an
               | exact line in my code where the error occurred, even
               | though I don't include function/file/line as-is.
        
               | kelnos wrote:
               | I don't really agree. Well, I do agree that often if I'm
               | looking at a backtrace, I will be skipping over a lot of
               | stack frames to find the "simplified" path that still is
               | most useful.
               | 
               | But functions? Yep, absolutely need them. Files? Not
               | quite so much, since it's rare that I'd use the same
               | function name between files. (But sure, throw it in
               | anyway.) Line numbers? No, those can be a big help. If a
               | user reports an issue to me, the first thing I will ask
               | them (if they didn't fill out the issue template
               | properly) is what version they're using (and what git
               | hash, if they've self-compiled from a random git
               | checkout). So I can check out the same version on my
               | laptop, and having a line-accurate trace can be very
               | helpful.
               | 
               | > _To reiterate my point from above though, my error
               | stacks are all unique_
               | 
               | To reiterate mine, my error stacks often aren't unique,
               | and crafting them such that they would be seems like
               | pointless make-work when there are tools can make it so I
               | don't need to care about this.
               | 
               | I really don't get this resistance against including this
               | information. It adds little to binary size and remove
               | little from performance, so why not include it? I agree
               | that backtraces do add a lot to binary size and can
               | murder performance, but this "StackedError" concept with
               | function/file/line information seems like essentially the
               | perfect compromise. Just... include it, and stop worrying
               | about it.
        
             | kelnos wrote:
             | The problem with encoding only "what the program is trying
             | to do" in the error is that it only helps users when it's
             | an "expected" situation. For the "open the configuration
             | file" example, it's usually something the user can
             | understand and fix on their own: file is missing, bad
             | permissions, etc.
             | 
             | But errors also need to be useful when reporting bugs to
             | the author of the software. Error context and the error
             | message can't always tell me what specific call stack
             | caused the error, and I will most likely need that when
             | tracking it down. I hesitate to want a backtrace included,
             | as generating those is usually bad for performance, but I
             | think SNAFU's "location" concept is a great compromise.
             | 
             | I see your reply further down about "Users rarely have good
             | access to the developer team", but I just don't buy that
             | line of reasoning. As a developer, I both want to make it
             | as easy as possible for my users to solve problems on their
             | own (so: informative error messages that give the user a
             | good chance of figuring it out themselves), but I'm only
             | human, and I know all the software I write has bugs. So I
             | want my error reporting to have enough information such
             | that the user can contact me and give me as much
             | information as possible about the error, without needing a
             | lot of back and forth, or without me needing to ask them to
             | run things in a debugger or use a special build.
             | 
             | And on top of that, a lot of code is written inside a
             | company, either as a network service, or tooling used only
             | by people inside that company. The developers are very
             | close to the use of that code, and having a lot of
             | information come with errors is essential.
             | 
             | > _My error stacks should always be unique -- any stack can
             | exactly point to a trace through my program._
             | 
             | That seems like more effort expended when using
             | `snafu::Location` would suffice, without doing extra work
             | that is IMO useless. I'd rather concentrate on other things
             | and have my tools do fiddly, repetitive work for me.
        
         | kibwen wrote:
         | _> 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 wrote:
           | 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.
        
           | ekimekim wrote:
           | That's interesting. To me stack traces + default pass up the
           | stack _are_ the distinguishing features of exceptions.
           | 
           | Suppose we had a version of the ? operator that automatically
           | appended a call stack to the error value returned. Are you
           | saying that that's not "an exception" because I still need to
           | write ? after each falliable function? Or because it's still
           | part of the return type? Or is it specifically only an
           | exception if it works via stack unwinding?
        
         | jgilias wrote:
         | 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?
        
         | zokier wrote:
         | 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
        
         | anon-3988 wrote:
         | I have a theory that what people actually want is something ala
         | named exceptions + forced try catch with pattern matching +
         | automaitally derived return Type.
        
         | PittleyDunkin wrote:
         | > 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
        
         | IshKebab wrote:
         | > 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.
        
         | ragnese wrote:
         | You're not far off. This is one of my favorite topics in
         | programming language design discussions, and I have opinions
         | that some may even say are "controversial". For what it's
         | worth, I've been writing Rust in production since 2016 (not
         | 100% of my time since then, but I've had a good amount of
         | experience with some decently long-lived projects of varying
         | complexity).
         | 
         | First, I assert that Java's checked exceptions are a solidly
         | _good_ feature. Of course it has flaws. The whole rest of the
         | language is also full of flaws, so that 's not surprising.
         | 
         | Second, I assert that there are two things that have caused the
         | vast majority of hate toward Java's checked exceptions:
         | programmers not being taught/shown how and when they're
         | intended to be used, and that oft-circulated interview
         | transcript from 2003 where Anders Hejlsberg asserts that
         | checked exceptions are language design "dead end". I don't
         | think he was right in 2003, and I especially don't think the
         | opinion is correct today in light of how much strong static
         | typing has really gained favor with the programming community.
         | But, that opinion really took off and we spent years and years
         | seeing that assessment repeated as a truism, which I think is
         | why it took so long to finally start experimenting with
         | statically typed failure modes again (e.g., Rust and Swift).
         | 
         | Now, here's where I'll get controversial about Rust error
         | handling. I'll try _really_ hard to keep this from turning into
         | an entire dissertation, but I 'll elaborate if anyone asks.
         | 
         | It is often a mistake to implement the `From` trait for error
         | types and use the `?` operator everywhere. Error types in an
         | API need to be aware of the context in which they occur, so
         | just converting by type only often doesn't make sense. You may
         | encounter a `FooError` type while your app is doing _totally_
         | different things, so it 's likely that not every `FooError`
         | occurrence _means_ the same thing to whoever is calling into
         | your code. Also, sometimes you can actually _handle_ an error,
         | and getting into the muscle memory habit of just tacking `?` on
         | to everything can lead to mistakenly propagating errors that
         | you might have better handled by doing something else
         | (including perhaps panicking).
         | 
         | There does seem to be a trend toward automatically adding stack
         | traces in Rust errors. This is completely misguided, IMO. And
         | this may be my MOST controversial opinion: stack traces almost
         | *never* belong in a `Result<>` error type. Result types should
         | be relevant to your "domain" (borrowing the term from "Domain
         | Driven Design" even though I do NOT advocate for DDD in
         | general).
         | 
         | Think about it this way: designing an API is about abstraction.
         | So if you write a integer division function that takes two
         | arguments and divides them, it might return `Result<i64,
         | DivideByZero>`. If the caller passes in a 0 divisor, then what
         | business is it of theirs to see what your private functions are
         | called, how many of them are called, and what line of your file
         | they were defined on? That's the leakiest of leaky
         | abstractions.
         | 
         | You might be thinking: "But, if I see an result/error value
         | that I didn't expect while running my program, the stack trace
         | will help me track down the issue!" Yeah, no kidding. So, let's
         | _also_ start adding stack traces to our successful values, too!
         | After, all, if I call my division function and get back a
         | `Result::Ok` with a weird number that I didn 't expect, I might
         | want to trace that back, too, right? (This suggestion is
         | sarcastic to prove a point. It should, hopefully, sound
         | ridiculous to add stack traces to every return value from every
         | function.)
         | 
         | The issue is that Rust's Result (and Java's checked exceptions)
         | require a different paradigm. A Result is in the type signature
         | because it's part of your domain's API design. It's just
         | values. It's not *for* debugging. You use a debugger for that
         | or programmatically panic when something is truly unexpected
         | and get the stack trace from that.
         | 
         | Which leads to the corollary to the previous controversial
         | opinion: Rust has unchecked exceptions; they're called panics
         | and they are 100% *okay to use* in the vast majority of
         | applications that the vast majority of day-job programmers work
         | on.
         | 
         | Obviously, context matters, and there are some places where
         | panicking is unacceptable. But, Result is for expected domain
         | failures. Panics are for programmer errors and unrecoverable
         | constraint violations. And I'm not advocating for panics to be
         | "lazy". Rust code that refuses to ever panic (as far as they
         | know, but I hope they aren't indexing any vecs/arrays just in
         | case!) usually leads to overly polluted error types where it
         | ends up being difficult to understand what errors are actually
         | meaningful and what errors are never actually going to happen.
         | Instead of inspecting errors and figuring out which to handle
         | and how, I've seen things just snowball into a giant mess of
         | nested enums with sometimes redundant error "branches" and
         | missed opportunities to actually handle some cases. If you, as
         | the programmer, know for sure that you just added something to
         | a HashMap earlier in your function and you know you didn't
         | remove it, then for the love of all things sacred, just write
         | `map.get("my-key").unwrap()` (or
         | `.expect("message")`--whatever) instead of making the caller
         | have to consider an error that will never happen, is not their
         | fault, and that they can't do anything about!
         | 
         | And, if you do have a situation where panicking is unacceptable
         | (you must be using `#![no_std]`, right??), then don't make a
         | bunch of different error types for all of the possible
         | programmer bugs. Just make a single umbrella `FatalError` type
         | and use that.
         | 
         | For further reading, I really like this piece from the book
         | Real World OCaml, which also has a Result type and exceptions:
         | https://dev.realworldocaml.org/error-handling.html.
         | Specifically, the very last section at the bottom of the page,
         | titled: "Choosing an Error-Handling Strategy". (The old version
         | of that page used to be more plain HTML and the sections had
         | anchors so I could link directly to that section...)
         | 
         | And for further reading about error handling strategy in a no-
         | panic context, I really like the approach described here:
         | https://sled.rs/errors
        
           | ThatGeoGuy wrote:
           | _You might be thinking: "But, if I see an result/error value
           | that I didn't expect while running my program, the stack
           | trace will help me track down the issue!" Yeah, no kidding.
           | So, let's also start adding stack traces to our successful
           | values, too! After, all, if I call my division function and
           | get back a `Result::Ok` with a weird number that I didn't
           | expect, I might want to trace that back, too, right? (This
           | suggestion is sarcastic to prove a point. It should,
           | hopefully, sound ridiculous to add stack traces to every
           | return value from every function.)_
           | 
           | I don't think I disagree with the ends you're proposing
           | (don't add stack traces to every value, don't add stack
           | traces specifically to Result::Err(E) variants); however,
           | this is a bad way to justify it. Tools like dtrace / bpftrace
           | do exactly this kind of stack tracing for both success and
           | error cases across entire systems. This is a good thing(tm),
           | and is actually very useful for both debugging, performance
           | profiling, and understanding what your code is really doing
           | on the hardware.
           | 
           | So I guess I disagree with how you're framing it. I would
           | argue that adding stack traces to every value in Rust would
           | be bad because it is a lot of overhead for something your
           | kernel can and will do better.
           | 
           |  _The issue is that Rust 's Result (and Java's checked
           | exceptions) require a different paradigm. A Result is in the
           | type signature because it's part of your domain's API design.
           | It's just values. It's not _for* debugging. You use a
           | debugger for that or programmatically panic when something is
           | truly unexpected and get the stack trace from that.*
           | 
           | This really is the gist of it. However, I will say that in my
           | experience the reason that Result types are nice (over e.g.
           | exceptions) is that putting the error cases in the type
           | contract means that you can have the compiler check when
           | someone hasn't handled an error case (? and unwrap are
           | "handling" it even if they may not always be appropriate), as
           | well as statically verify which variants may be unused. One
           | very frustrating thing I've had to encounter in C++ is
           | finding a whole list of different errors that have been
           | duplicated as multiple different opaque (e.g. behind a
           | unique_ptr<std::exception> or some such) exceptions across
           | the codebase.
           | 
           | Being able to know what variants of error can come out of an
           | API is great! It just happens that working with a rich type
           | system like Rust makes it possible to do all manner of things
           | that languages-with-only-exceptions cannot.
        
             | ragnese wrote:
             | Yeah, fair point about dtrace, et al, but I think my
             | statement is still fine in context, since we're
             | specifically talking about these Rust libraries that
             | collect stack traces for error types.
             | 
             | And I agree and love having statically checked failure
             | modes! So, if you're choosing to panic in Rust, it better
             | be because of something that is really not able to be
             | handled at all (caveat: the top-level event loop or
             | whatever could catch panics/exceptions, print a "Oops!
             | Something went wrong!" message to the user and then either
             | die or try to keep going, etc, but no handling
             | panics/exceptions in "middle" layers.).
        
           | agos wrote:
           | characterizing people who think checked exceptions as either
           | bad programmers or unable to have their own opinion on the
           | matter does not do a great service to your argument
        
             | ragnese wrote:
             | Yeah, that whole statement there is probably unnecessary
             | and I can see it being off-putting. I'll edit it if I still
             | can.
             | 
             | However, I just want to make it clear that I wasn't
             | intending to call anyone a "bad programmer". At least not
             | in a personally insulting way. We've all been in a position
             | where we were uninitiated at something. And most of us have
             | been in a situation where we've jumped into a new
             | programming language without having any kind of "formal"
             | education on the design, philosophy, and intended best
             | practices. For example, with Java, one _should_ read
             | documents like: https://docs.oracle.com/javase/tutorial/ess
             | ential/exceptions..., especially this part: https://docs.or
             | acle.com/javase/tutorial/essential/exceptions....
             | 
             | So, again, that part wasn't actually meant as an insult.
             | We're all uneducated about many things at every point in
             | our lives. And I think that lack of education or guidance
             | on designing error types and handling has caused a lot of
             | people to end up burying themselves in checked exception
             | hell, and dismissing the whole thing because of that
             | frustration.
             | 
             | The other part about cargo-culting... well, yeah, that was
             | me insulting people.
        
           | ekimekim wrote:
           | > Result is for expected domain failures. Panics are for
           | programmer errors and unrecoverable constraint violations.
           | 
           | The problem is that "unrecoverable constraint violations"
           | happen a lot in practice when you're dealing with
           | filesystems, networking...anything that isn't pure
           | computation.
           | 
           | Suppose I have a function that calls other functions that
           | themselves make 3 database queries, two HTTP requests, and
           | reads/writes from a cache directory. It considers all of them
           | (except perhaps the caching) unrecoverable in the context of
           | that function. What should it do?
           | 
           | I see three reasonable options:
           | 
           | (1). return a simple error type saying "Networking failure",
           | "IO Error", etc if any of those fail
           | 
           | (2). return a complex error type that exposes the internal
           | details of all the different things it's doing and which one
           | failed and why
           | 
           | (3). panic if any of them fail
           | 
           | I would argue that (1) is unfit for purpose as you have no
           | idea what's actually going wrong.
           | 
           | And (3) is currently very heavily discouraged, though I think
           | if I'm understanding your argument right it probably makes
           | the most sense. However it leaves your top-level function in
           | the awkward position of needing to make that panic part of
           | its API contract, without the type system to help. It's also
           | highly limiting because the caller now can't distinugish
           | between programmer errors and possibly-transient
           | environmental conditions like a service outage.
           | 
           | (2) is what I'd expect to see in practice right now, and
           | that's what leads to these automatic stack traces, etc. But
           | none of these feel like good options. Ideally I'd want
           | something that is:
           | 
           | - Debuggable (like (2) and (3))
           | 
           | - Part of the type system (like (1) and (2))
           | 
           | - Still allows introspection by the caller (like (1) and (2))
           | 
           | - Doesn't require a ton of boilerplate at each level (like
           | (3), and possibly (1))
           | 
           | (edited for formatting)
        
             | kelnos wrote:
             | No, I don't think you understood the GP's argument. Network
             | and filesystem errors are not always "unrecoverable
             | constraint violations". They're often just simple errors --
             | things that you should expect to happen, even -- and your
             | (1), or, better, (2), are the most appropriate reactions to
             | those.
             | 
             | "Unrecoverable constraint violations" occur, for example,
             | when you've done a sanity check on some data structure and
             | found that it's in a state that should be impossible, and
             | so continuing from there is unsafe.
             | 
             | Even then, you may choose to handle them in a better way
             | than simply aborting the program. For example, if I'm
             | writing a HTTP service that is backed by a database, and I
             | get a customer request that results in me finding that a
             | column in the database is NULL when it shouldn't be, I'll
             | probably just return a 500 error to the customer rather
             | than panic!(). The assumption is that even though there's a
             | problem with this particular data, that might be the result
             | of an almost-never-hit edge case, and we can still serve
             | other customer requests just fine.
             | 
             | Sure, a simple single-user command-line application may
             | choose to panic!() if a critical data file can't be opened
             | from the filesystem. Maybe that is an "unrecoverable
             | constraint violation" sometimes. But I think there's a lot
             | of nuance you're missing.
        
           | kelnos wrote:
           | > _First, I assert that Java 's checked exceptions are a
           | solidly good feature._
           | 
           | I agree in theory, but I think they're very poorly
           | implemented, and the syntax and tooling around handling them
           | is terrible. And, frankly, those flaws (yes, I agree
           | everything has flaws) make the overall feature mostly
           | useless, unfortunately. It really doesn't matter where you
           | think all the hate comes from; the hate is there, and it
           | means that very few people use checked exceptions, except for
           | where they're required to when stdlib methods throw them.
           | Ultimately that's all that matters. If no one uses the
           | feature, then it's not a useful feature, regardless of the
           | reasons.
           | 
           | > _The issue is that Rust 's Result (and Java's checked
           | exceptions) require a different paradigm. A Result is in the
           | type signature because it's part of your domain's API
           | design._
           | 
           | Correct, but in Java, checked exceptions are _also_ a part of
           | the API and ABI, so there 's really little difference there,
           | outside of ergonomics. (Which IMO are one of the most
           | important parts!)
           | 
           | > _(This suggestion is sarcastic to prove a point. It should,
           | hopefully, sound ridiculous to add stack traces to every
           | return value from every function.)_
           | 
           | I don't think that proves a point. Sure, you can argue every
           | proposal into absurdity; it doesn't make the suggestion
           | itself bad.
           | 
           | > _Rust has unchecked exceptions; they 're called panics and
           | they are 100% _okay to use* in the vast majority of
           | applications that the vast majority of day-job programmers
           | work on.*
           | 
           | Yes, and this really bothers me. I wish more people would
           | annotate their functions with `#[no_panic]`. Actually, I wish
           | that was the default, and if you want to write a function
           | that panics or calls functions that can panic, you need to
           | annotate the function with `#[can_panic]`, and the compiler
           | should enforce that, and `rustdoc` should surface that in all
           | documentation.
        
         | packetlost wrote:
         | Checked exceptions that don't automatically propagate up the
         | call stack to be specific. There's a subtle but _incredibly_
         | important difference between just  "exceptions" and what you're
         | describing.
        
         | tonyhart7 wrote:
         | You can use 1 type of error enum for your app
         | 
         | for example me, Yes my code can fail and only have 1 type eg:
         | AppError
         | 
         | but I can supplement that with db error,cache error,serde error
         | etc
        
         | maxk42 wrote:
         | There's a critical difference between exceptions and what's
         | happening in this article: exceptions create de facto
         | nondeterministic behavior in programs. They cause every line in
         | a function to potentially result in a return from the function
         | with an unexpected type. Rust's error handling requires
         | explicit return statements and explicit return types. This
         | critical difference results in code that is far easier to
         | document, reason about, and slightly better performance as
         | well.
        
           | kelnos wrote:
           | GP specifically said _checked_ exceptions, which don 't
           | create the problems you describe. (They do create _other_
           | problems, of course.)
           | 
           | And exceptions don't have to be slower than putting errors in
           | return values.
           | 
           | (Having said that, I am still not a proponent of exceptions
           | for error handling.)
        
         | ljm wrote:
         | Go's approach has been to treat errors as a linked list, and
         | thus one would explicitly create a chain of errors by wrapping
         | each one as it passes up the stack. The end result would be an
         | error like 'Error Z: Error Y: Error X', as each error in the
         | list is 'unwrapped'.
         | 
         | The lack of any kind of caller information when creating an
         | error makes it quite important to write decent error messages,
         | which I think is actually quite hard to do.
         | 
         | At the same time I think it depends on what you're building: a
         | library should have good errors (ideally well-typed ones too),
         | but in an application you'd benefit from adding logging at each
         | point in the stack (which can then contain caller information
         | like file and line number) rather than just doing the logging
         | at a system boundary; maybe set it at debug level. Then use
         | tracing for the rest of it (for extra visibility in stuff like
         | Sentry).
         | 
         | At least, I feel like that's how you'd be encouraged to do it
         | in Go considering the opinions of Go's creators.
        
         | Groxx wrote:
         | Java's main issue is that its `throws` isn't generic. It forces
         | middleware-like code to choose between `throws Exception` and
         | runtime-only plus boxing... both of which lose ALL details and
         | ruin your compile-time safety.
         | 
         | IMO it just poisoned the well, and now everyone* _thinks_ they
         | don 't like checked exceptions, when really they just don't
         | like Java's badly crippled version.
        
           | ragnese wrote:
           | You can have generic `throws` markers; e.g.,
           | interface Frobinicator<E extends Exception> {
           | void frobinicate() throws E;         }
        
             | Groxx wrote:
             | Which gives you a single exception type, not a list.
             | Squashing the list of possibilities rather uselessly.
             | 
             | You can work around this with N `T extends Exception`s, but
             | now you have to pick the correct one all the time. And e.g.
             | using it in a `map`-style stream with a final collected
             | throw means picking whether you're adding type N or not. Or
             | possibly multiple new types. It rapidly grows to be
             | unusable.
             | 
             | You also can't make a `class MyException<T>`. Or do a
             | `catch (T e)`. There are a lot of blockages in practice to
             | trying to do any of this - exceptions are very special in
             | the type system, which is the problem.
        
         | kelnos wrote:
         | I get what you're saying, but this is still very different from
         | (checked) exceptions, both in syntax and ergonomics.
         | 
         | Java's checked exceptions are the worst. Having to declare
         | every exception thrown as a part of your API/ABI makes for
         | brittle, difficult-to-evolve interfaces.
         | 
         | Rust's Result and '?' syntax sidesteps a few of these issues.
         | You can "add" underlying errors to the error return of your
         | function without changing its API/ABI. You don't need to add a
         | bunch of try/catch blocks, cluttering and confusing the code,
         | in order to make sense of this and convert exceptions into
         | whatever your API/ABI specifies. Rust's 'From<>' trait is damn-
         | near magical when it comes to error conversion and propagation.
         | 
         | I get that not everyone is a functional programming enthusiast,
         | but you can't do FP with exceptions. (Well, you can, via a sort
         | of Try monad like Scala has, but it's error-prone and ugly to
         | deal with.) With Result, you can, and it works seamlessly with
         | the rest of the language and syntax.
         | 
         | I don't think Rust's error model is perfect, but it's miles
         | ahead of what I've worked with in most other languages.
        
       | DavidWilkinson wrote:
       | 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.
        
       | gregwebs wrote:
       | This seems like a user implement of Zig error return traces:
       | https://ziglang.org/documentation/master/#Error-Return-Trace...
        
       | k_bx wrote:
       | The simplest thing that "just work" for me is replacing ? with
       | .context(h!())? and this macro:
       | 
       | #[macro_export]
       | 
       | macro_rules! h {                   () => {
       | concat!("at ", file!(), " line ", line!(), " column ", column!())
       | };
       | 
       | and then using anyhow::Result.
       | 
       | Solves 99% problems in error handling
        
       | dmart wrote:
       | Using #[from] in a thiserror enum is an antipattern, IMO. I kind
       | of wish it wasn't included at all because it leads people to this
       | design pattern where errors are just propagated upwards without
       | any type differentiation or additional context.
       | 
       | You can absolutely have two different enum variants from the same
       | source type. It would look something like:
       | #[derive(Debug, Error)]         pub(crate) enum MyErrorType {
       | #[error("failed to create staging directory at {}",
       | path.display())]             CreateStagingDirectory{
       | source: std::io::Error,                 path: std::path::PathBuf,
       | },                  #[error("failed to copy files to staging
       | directory")]             CopyFiles{                 source:
       | std::io::Error,             }         }
       | 
       | This does mean that you need to manually specify which error
       | variant you are returning rather than just using ?:
       | create_dir(path).map_err(|err|
       | MyErrorType::CreateStagingDirectory {             source: err,
       | path: path.clone()          })?;
       | 
       | but I would argue that that is the entire point of defining a
       | specific error type. If you don't care about the context and only
       | that an io::Error occurred, then just return that directly or use
       | a type-erased error.
        
         | shepmaster wrote:
         | This is one of the things I like about SNAFU: it makes this
         | preferred pattern the default and makes it nicer to use. For
         | example, your usage would look something like this with SNAFU:
         | create_dir(path).context(CreateStagingDirectorySnafu { path
         | })?;
         | 
         | Note a few points:
         | 
         | 1. No need to use the closure
         | 
         | 2. No need to carry the source error over yourself (`context`
         | does this for you)
         | 
         | 3. No need to explicitly call `clone` on the path (`context`
         | does this for you)
        
       | carlsverre wrote:
       | Inspired by this blog post I just added an `#[implicit]` field
       | feature to the `thiserror` crate. It makes it easy to
       | automatically annotate errors with things like code location (per
       | this blog post), a timestamp, or a backtrace without requiring
       | further modifications to the thiserror crate. I'm hoping that
       | dtolnay will consider it. You can find my PR here:
       | https://github.com/dtolnay/thiserror/pull/402
        
       ___________________________________________________________________
       (page generated 2024-12-19 23:02 UTC)