[HN Gopher] Stop Forwarding Errors, Start Designing Them
       ___________________________________________________________________
        
       Stop Forwarding Errors, Start Designing Them
        
       Author : andylokandy
       Score  : 50 points
       Date   : 2026-01-04 19:02 UTC (3 hours ago)
        
 (HTM) web link (fast.github.io)
 (TXT) w3m dump (fast.github.io)
        
       | bheadmaster wrote:
       | Many Rust programmers despise Go's "if err != nil" pattern, but
       | that pattern actually forces you to think about errors and
       | "design" them to give meaningful messages, either by wrapping
       | them (if the underlying error is expected to provide userful
       | information), or by creating a one from scratch.
       | 
       | It may be easier to just add the "?" operator everywhere (and we
       | are lazy and will mostly do what is easier), but it often leads
       | to problem explained in the article.
        
         | jayknight wrote:
         | >that pattern actually forces you to think about errors and
         | "design" them to give meaningful messages
         | 
         | Doesn't Rust's Result type(s) force you to do the same? Sure,
         | you can pass them on with the ? operator, but it's still a
         | choice you have to make.
        
         | alembic_fumes wrote:
         | _Hard_ disagree. Most of the Go code that I 've ever worked
         | with has been littered with one or another variant of the
         | following:                 value, err := doFallibleOperation()
         | if err != nil {         return nil, fmt.Errorf("fallible
         | operation failed - %w", err)       }
         | 
         | That error construct exclusively works for the poor human who
         | has to debug the system, looking at its logs. No call stacks
         | and, crucially, no automatic handling.
         | 
         | At least with Rust's enums it is possible to make errors
         | automatically actionable. If one skips that part and opts for
         | anyhow because it's too much work, that's really a user
         | problem.
         | 
         | I like the author's idea of "designing" errors by exposing
         | their actionability in the interface a lot. I'm not overall
         | sold on whether that should be the primary categorization, but
         | _at least_ including a docstring to each enum variant about
         | what can be done about the matter sounds like a nice way to
         | improve most code a little bit.
        
           | Fizzadar wrote:
           | As a primarily Go dev - 100% agree. The endless check and
           | wrap error results in long chains of messages you have to
           | grep for to understand the call stack. For what benefit?
           | Might as well just panic and recover/log the stack in many
           | cases.
        
             | formerly_proven wrote:
             | Artisanal callstacks
        
             | morshu9001 wrote:
             | The error handling is by far my least favorite aspect of
             | Go. It's tedious and dangerous. It should either be like
             | Rust or like JS, there isn't a good third option.
        
               | tcfhgj wrote:
               | what about checked exceptions (Java)?
        
           | bheadmaster wrote:
           | > If one skips that part and opts for anyhow because it's too
           | much work, that's really a user problem.
           | 
           | If a language makes this more convenient than doing it right,
           | one could argue that the language design is at fault.
        
           | Thaxll wrote:
           | In many code base you have custom errors that implement the
           | error interface ( for http code and the like ), it's very
           | common.
        
         | akdor1154 wrote:
         | I think that was the intent of Go's design, but in practise i
         | think it normally devolves into an overly verbose '?' with a
         | poorly typed Result<_, String>.
         | 
         | As a Go dev, I'm looking at this article with great interest. I
         | would very much like to apply this approach to Go as well, I
         | think the author has got a very strong design there.
        
       | vaylian wrote:
       | I've been thinking about Rust errors as well. We see all these
       | nice tutorials that explain how you can match on an Err and then
       | handle it. But I haven't seen this being done in practise. Most
       | errors are reported directly to the user. There don't seem to be
       | any attempts to automatically handle them.
       | 
       | The cause for an error can be upstream or downstream. If a
       | function fails, because the network is down, then this is a
       | downstream error. The user has not done anything wrong (unless
       | they also are responsible for the network infrastructure). In
       | that case a retry after a few moments might be the right
       | approach. However, if the user provides bad function arguments,
       | then the user needs to be informed, that it's them who need to
       | make corrections. However, it is not always clear if that is the
       | case. If a user requests a non-existing file, then there might be
       | different reasons why the file does not exist (yet).
        
         | rileymat2 wrote:
         | I am a bit confused by the network example, even when I don't
         | control the network at the moment I need to do something about
         | it and know about it to act.
        
       | fozem wrote:
       | Good overview on Rust error handling.
       | 
       | I like errors that are unique and trivially greppable in a
       | codebase. They should be stack efficient and word sized. Maybe a
       | new calling convention where a register is reserved for error
       | code and another register is a pointer to the source location
       | string that is stored in a data segment.
       | 
       | The FP fanboy side of me likes the idea of algebraic effects and
       | ADTs but not at the expense of stack efficiency.
        
         | EPWN3D wrote:
         | You basically want a modern errno. I don't mean that as a dig
         | at you -- I've found POSIX error codes to still be the best way
         | to design errors in C. If it can't be evaluated by switch, then
         | it's too complicated.
        
       | Rygian wrote:
       | Sorry for the small digression. It's on topic.
       | 
       | Just a few minutes ago, while copying 63 GB worth of pics and
       | videos from my phone to my laptop, KDE forwarded me the error
       | "File <hard to retain name.jpg> could not be opened. Retry,
       | Ignore, Ignore all, Cancel".
       | 
       | This was around file 7000 out of 15000. The file transfer stopped
       | until I made a choice.
       | 
       | As a user, what am I supposed to do with such a popup?
       | 
       | It seems like a very good example of "Eror Handling Without
       | Purpose" as the article describes, but at user level.
       | 
       | Except that here, the audience is "a plain user who just dragged
       | a folder to make a copy" and none of the four options (or even
       | the act of stopping the file transfer until an answer is chosen)
       | is actually meaningful for the user.
       | 
       | The "Putting It Together" for this scenario should look like: a
       | non-modal section populates with "file <hard to retain name.jpg>
       | failed due to reason; at the end of the file transfer you'll get
       | a list with all the files that failed, and you'll have an option
       | to retry them, navigate to their source position to double-check,
       | and/or ignore".
        
         | XorNot wrote:
         | This design still doesn't work: what if the user walks away and
         | the computer is powered off in the meantime?
         | 
         | I.e. you need to write the report of this to a file itself. In
         | fact you should allocate a decently large file upfront to make
         | sure you _can_ write the report and the error message (out of
         | disk space for example).
        
           | throw-the-towel wrote:
           | And what if the computer is kidnapped by the US Army while
           | it's copying the files?
           | 
           | You just can't defend against everything, but an imperfect
           | solution can still be an improvement over the status quo.
        
             | XorNot wrote:
             | No, but imagine doing all the work to collect up a list of
             | files that failed only to say, pop a modal at the end of
             | the process that coincides with the user hitting Enter
             | because they were multitasking and it auto-accepts the
             | dialog. Information gone, context lost, in fact your entire
             | design has failed to change the experience at all! All
             | because of one UI overlap that's actually very common.
             | 
             | We have shared workstations for example where this would be
             | a typical use case for non-tecchnical users across multiple
             | user logins: ensuring you can check that the big data
             | transfer was complete a few hours later would be very
             | useful, but if you only do a fraction of the work for
             | completeness then again, it's of no benefit.
        
           | Rygian wrote:
           | It goes quite far, actually.
           | 
           | A file transfer should remain active even if both devices
           | (source, destination) are physically disconnected, or in
           | network partitions, or when devices are full, need media
           | change, etc.
           | 
           | The only valid states for a file transfer are: ongoing, fully
           | completed with 100% success, or explicitly cancelled by the
           | user with a full usable report of what got copied, fully or
           | partially, and what did not get copied.
           | 
           | The file transfer dialogs and tooling of today's mainstream
           | computing are stuck in the nineties.
        
       | Sytten wrote:
       | Exn looks very interesting, but to be actionable we need a
       | compatibility layer with thiserror and anyhow since most are
       | using it right now. Moving the goalpost a little we mostly need a
       | core rust solution otherwise your error handling stops at the
       | first library you use that doesn't use exn.
        
       | dvogel wrote:
       | > But as a standard library abstraction, it's too opinionated. It
       | categorically excludes cases where sources form a tree: a
       | validation error with multiple field failures, a timeout with
       | partial results. These scenarios exist, and the standard trait
       | offers no way to represent them.
       | 
       | This seems akin to complaining that the CPU core has only one
       | instruction pointer. There is nothing preventing a struct
       | implementing `Error` from aggregating other errors (such as
       | validation results) and still exposing them via the `Error`
       | trait. The fact of the matter is that the call stack is linear,
       | so the interior node in the tree the author wants still needs to
       | provide the aggregate error reporting that reflects the call
       | stack that was lost with the various returns. Nothing about that
       | error type implementing `Error` prevents it from also
       | implementing another error reporting trait that reflects the
       | aggregate errors in all of the underlying richness with which
       | they were collected.
        
       | oncallthrow wrote:
       | This is interestingly somewhere where Go really shines, in my
       | experience. Go has no requirement to wrap (or, indeed, even
       | handle at all) errors; yet, despite this, Go codebases I've
       | worked in almost always perform error handling properly (wrapping
       | at each layer of the call stack, so it's easy to identify where
       | an error occurred).
        
         | morshu9001 wrote:
         | I'd rather have exceptions so this is done for you. Not really
         | an option in Rust due to overhead ofc.
        
         | jiehong wrote:
         | For the flat structure part, it's much less shiny, though.
         | 
         | Weirdly, the last time I saw an error in production I couldn't
         | investigate was because of a go service with no error
         | wrapping... funny coincidence
        
       | Thaxll wrote:
       | Looks very similar to what Upspin ( Go ) errors look like:
       | 
       | https://github.com/upspin/upspin/blob/master/errors/errors.g...
       | type Error struct {             // Path is the Upspin path name
       | of the item being accessed.             Path upspin.PathName
       | // User is the Upspin name of the user attempting the operation.
       | User upspin.UserName             // Op is the operation being
       | performed, usually the name of the method             // being
       | invoked (Get, Put, etc.). It should not contain an at sign @.
       | Op Op             // Kind is the class of error, such as
       | permission failure,             // or "Other" if its class is
       | unknown or irrelevant.             Kind Kind             // The
       | underlying error that triggered this one, if any.             Err
       | error                  // Stack information; used only when the
       | 'debug' build tag is set.             stack         }
        
       | croemer wrote:
       | Be warned: LLM writing. Lots of negative parallelisms.
        
       | nchagnet wrote:
       | I really like the pattern presented in the article. I find myself
       | guilty of designing errors which are useful to me, but maybe not
       | to my user (which tbh in my area is always a bit of a nebulous
       | entity). I really like the idea of separating those two intents,
       | and to make explicit the possible action.
        
       | jiehong wrote:
       | I suppose Java exceptions have the same issues, albeit with
       | automatic stack traces, obviously:
       | 
       | - the ? keyword is replaced either by runtime exceptions and so
       | each function do it transpires you don't catch it, or by simply
       | stating the raised exception in the signature
       | 
       | - message can be overloaded for humans
       | 
       | - the exception type itself is the structured data, but in
       | practice it seldom contains structured data and most logic
       | depends on the exception type.
       | 
       | Make of this what you will, but I didn't say it's great.
        
       | bccdee wrote:
       | I'm not sure I like how they're trying to dynamically cast to an
       | error type.                 Err(report) => {           // For
       | machines: find and handle the structured error           if let
       | Some(err) = find_error::<StorageError>(&report) {
       | if err.status == ErrorStatus::Temporary {
       | return queue_for_retry(report);               }
       | return Err(map_to_http_status(err.kind));           }
       | 
       | They get it right elsewhere when they describe errors for
       | machines as being "flat and actionable." `StorageError` is that,
       | but the outer `Err(report)` is not. You shouldn't be guessing
       | which types of error you might run into; you should be
       | exhaustively enumerating them.
       | 
       | I'd rather have something like this:                 struct
       | Exn<T> {           trace: Trace,           err: T,       }
       | impl<T> Exn<T> {           fn wrap<U: From<T>>(self, msg: String)
       | -> Exn<U> {               Exn {                   trace:
       | self.trace.with_frame(msg),                   err:
       | self.err.into(),               }           }       }
       | 
       | That way your `err` field is always a structured error, but you
       | still get a context trace. With a bit more tweaking, you can add
       | `#[track_caller]` and make the trace tree-shaped rather than
       | linear. I think actionable error types need to be exhaustively
       | matchable, though; I think that has to be the case for any error
       | that you expect a machine to be handling.
        
       | larusso wrote:
       | Error handling in rust is the number one frustration. I rewrote
       | my errors multiple time. I used error_chain which looked good on
       | paper but was just as broken as thiserror and anyhow. The missing
       | piece is already the fact that no one really defines how to write
       | good and meaningful error types for the different audiences. Even
       | the article described some cases that are highly implementation
       | specific. I will take a look at this other crate the author
       | showed though. The thiserror crate makes it too easy to just
       | foreward errors with the #from / #source implementations. I
       | played around with a helper crate that tries to add a context
       | method to each generated error types. But this as well is
       | optional and also adds tons of overhead.
        
       ___________________________________________________________________
       (page generated 2026-01-04 23:00 UTC)