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