logoalt Hacker News

(On | No) Syntactic Support for Error Handling

393 pointsby henrikhorlucklast Tuesday at 4:18 PM539 commentsview on HN

Comments

threemuxlast Tuesday at 5:24 PM

If you feel the need (as many have in this thread) to breezily propose something the Go Team could have done instead, I urge you to click the link in the article to the wiki page for this:

https://go.dev/wiki/Go2ErrorHandlingFeedback

or the GitHub issue search: https://github.com/golang/go/issues?q=+is%3Aissue+label%3Aer...

I promise that you are not the first to propose whatever you're proposing, and often it was considered in great depth. I appreciate this honest approach from the Go Team and I continue to enjoy using Go every day at work.

show 6 replies
pie_flavorlast Tuesday at 5:06 PM

You draw up a list of checkboxes, you debate each individual point until you can check them off, you don't uncheck them unless you have found a showstopping semantics error or soundness hole. Once it is complete it is implemented and then everyone who had opinions about whether it should be spelt `.await` or `/await` or `.await!()` vanishes back into the woodwork from whence they came. Where's the disconnect?

Rust works like this. Sometimes an issue can be delayed for over a decade, but eventually all the boxes are checked off and it gets stabilized in latest nightly. If Go cannot solve the single problem everyone immediately has with the language, despite multiple complete perfect proposals on how to do it, simply because they cannot pick between the proposals and are waiting for people to stop bikeshedding, then their process is a farce.

show 7 replies
_jablast Tuesday at 5:13 PM

I once had a Go function that, unusually, was _expecting_ an error to be returned from an inner function, and so had to return an error (and do some other processing) if none was returned by the inner function, and return nil if the inner function did return an error.

In a nutshell, this meant I had to do `if err == nil { // return an error }` instead of `if err != nil { ... }`. It sounds simple when I break it down like this, but I accidentally wrote the latter instead of the former, and was apparently so desensitized to the latter construct that it actually took me ages to debug, because my brain simply did not consider that `if err != nil` was not supposed to be there.

I view this as an argument in favor of syntactic sugar for common expressions. Creating more distinction between `if err != nil` (extremely common) and `if err == nil` (quite uncommon) would have been a tangible benefit to me in this case.

show 7 replies
jamamplast Tuesday at 6:11 PM

I like Go's explicit error handling. In my mind, a function can always succeed (no error), or either succeed or fail. A function that always succeeds is straightforward. If a function fails, then you need to handle its failure, because the outer layer of code can not proceed with failures.

This is where languages diverge. Many languages use exceptions to throw the error until someone explicitly catches it and you have a stack trace of sorts. This might tell you where the error was thrown but doesn't provide a lot of helpful insight all of the time. In Go, I like how I can have some options that I always must choose from when writing code:

1. Ignore the error and proceed onward (`foo, _ := doSomething()`)

2. Handle the error by ending early, but provide no meaningful information (`return nil, err`)

3. Handle the error by returning early with helpful context (return a general wrapped error)

4. Handle the error by interpreting the error we received and branching differently on it. Perhaps our database couldn't find a row to alter, so our service layer must return a not found error which gets reflected in our API as a 404. Perhaps our idempotent deletion function encountered a not found error, and interprets that as a success.

In Go 2, or another language, I think the only changes I'd like to see are a `Result<Value, Failure>` type as opposed to nillable tuples (a la Rust/Swift), along with better-typed and enumerated error types as opposed to always using `error` directly to help with error type discoverability and enumeration.

This would fit well for Go 2 (or a new language) because adding Result types on top of Go 1's entrenched idiomatic tuple returns adds multiple ways to do the same thing, which creates confusion and division on Go 1 code.

show 5 replies
maxwellglast Tuesday at 4:54 PM

This is the right move for Go. I have grown to really love Go error handling. I of course hated it when I was first introduced to the language - two things that changed that:

- Reading the https://go.dev/blog/errors-are-values blog post (mentioned in the article too!) and really internalizing it. Wrote a moderately popular package around it - https://github.com/stytchauth/sqx

- Becoming OK with sprinkling a little `panic(err)` here and there for truely egregious invalid states. No reason forcing all the parent code to handle nonsense it has no sense in handling, and a well-placed panic or two can remove hundreds of error checks from a codebase. Think - is there a default logger in the ctx?

show 5 replies
Mond_last Tuesday at 6:31 PM

I really don't like how this article claims that the primary issue with Go's error handling is that the syntax is too verbose. I don't really care about that.

How about:

- Errors can be dropped silently or accidentally ignored

- function call results cannot be stored or passed around easily due to not being values

- errors.Is being necessary and the whole thing with 'nested' errors being a strange runtime thing that interacts poorly with the type system

- switching on errors being hard

- usage of sentinel values in the standard library

- poor interactions with generics making packages such as errgroup necessary

Did I miss anything?

show 4 replies
tsimionesculast Tuesday at 5:36 PM

> Going back to actual error handling code, verbosity fades into the background if errors are actually handled. Good error handling often requires additional information added to an error. For instance, a recurring comment in user surveys is about the lack of stack traces associated with an error. This could be addressed with support functions that produce and return an augmented error. In this (admittedly contrived) example, the relative amount of boilerplate is much smaller:

  [...] 
  if err != nil {
        return fmt.Errorf("invalid integer: %q", a)
    }
  [...] 
It's so funny to me to call "manually supplying stack traces" as "handling an error". By the Go team's definition of handling errors, exceptions* "automatically handle errors for you".

* in any language except C++, of course

show 1 reply
JamesSwiftlast Tuesday at 5:10 PM

I havent followed this argument closely so forgive me if I'm missing relevant discussion, but I dont see why the Rust style isnt just adopted. Its the thing I immediately add now that I have generics in Go.

I only see this blurb in a linked article:

> But Rust has no equivalent of handle: the convenience of the ? operator comes with the likely omission of proper handling.

But I fail to see how having convenience equates to ignoring the error. Thats basically half of my problem with Go's approach, that nothing enforces anything about the result and only minimally enforces checking the error. eg this results in 'declared and not used: err'

  x, err := strconv.Atoi("123")
  fmt.Println("result:", x)
but this runs just fine (and you will have no idea because of the default 0 value for `y`):

  x, err := strconv.Atoi("123")
  if err != nil {
    panic(err)
  }
  y, err := strconv.Atoi("1234")
  fmt.Println("result:", x, y)
this also compiles and runs just fine but again you would have no idea something was wrong

  x, err := strconv.Atoi("123")
  if err != nil {
  }
  fmt.Println("result:", x)
Making the return be `result` _enforces_ that you have to make a decision. Who cares if someone yolos a `!` or conveniently uses `?` but doesnt handle the error case. Are you going to forbid `panic` too?
show 6 replies
d3ckardlast Tuesday at 6:19 PM

From the Elixir's developer perspective, this is insane. The issue is solved in Erlang / Elixir by functions commonly returning {:ok, result} or {:error, description_or_struct} tuples. This, together with Elixir's `with` statement allows to group error handling at the bottom, which makes for much nicer readability.

Go could just add an equivalent of `with` clause, which would basically continue with functions as long as error is nil and have an error handling clause at the bottom.

show 4 replies
reader_1000last Tuesday at 7:05 PM

> For instance, a recurring comment in user surveys is about the lack of stack traces associated with an error. This could be addressed with support functions that produce and return an augmented error.

Languages with stack traces gives this to you for free, in Go, you need to implement it every time. OK, you may be disciplined developer where you always augment the error with the details but not all the team members have the same discipline.

Also the best thing about stack traces is that it gives you the path to the error. If the error is happened in a method that is called from multiple places, with stack traces, you immediately know the call path.

I worked as a sysadmin/SRE style for many years and I had to solve many problems, so I have plenty of experience in troubleshooting and problem solving. When I worked with stack traces, solving easy problems was taking only 1-2 minutes because the problems were obvious, but with Go, even easy problems takes more time because some people just don't augment the errors and use same error messages which makes it a detective work to solve it.

pikzellast Tuesday at 6:35 PM

They still haven't solved shadowing.

  a, err := foo()
  b, err := bar()
  if err != nil { // oops, forgot to handle foo()'s err }
This is the illusion of safe error handling.
show 3 replies
hackingonemptylast Tuesday at 5:39 PM

Generators and Goroutines have keywords/syntax in Golang but now they don't want to pile on more to handle errors. They could have had one single bit of syntactic sugar, "do notation", to handle all three and more if they had considered it from the beginning but it seems too late if the language designers are even aware of it. TFA says "If you’re wondering if your particular error handling idea was previously considered, read this document!" but that document references languages with ad-hoc solutions (C++, Rust, Swift) and does not reference languages like Haskell, Scala, or OCaml which have the same generic solution known as do-notation, for-comprehensions, and monadic-let respectively.

For example instead of

  func printSum(a, b string) error {
      x, err := strconv.Atoi(a)
      if err != nil {
          return err
      }
      y, err := strconv.Atoi(b)
      if err != nil {
          return err
      }
      fmt.Println("result:", x + y)
      return nil
  }
they could have something like this:

  func printSum(a, b string) result[error, unit] {
      return for {
          x <- strconv.Atoi(a)
          y <- strconv.Atoi(b)
      } yield fmt.Println("result:", x + y)
  }

which desugars to:

  func printSum(a, b string) result[error, unit] {
      return strconv.Atoi(a).flatMap(func(x string) result[error, unit] {
          return strconv.Atoi(b).map(func(y string) unit {
              return fmt.Println("result:", x + y)
          }
      }
  }
and unlike ad-hoc solutions this one bit of syntax sugar, where for comprehensions become invocations of map, flatMap, and filter would handle errors, goroutines, channels, generators, lists, loops, and more, because monads are pervasive: https://philipnilsson.github.io/Badness10k/escaping-hell-wit...
show 2 replies
tslocumlast Tuesday at 7:00 PM

Seems strange that Rust's "?" gets a mention syntax-wise, but nothing is said about sum types coming to Go. Go's verbose error handling and lack of sum types are my only gripes with the language. It would be nice to see both addressed using Rust's Result type as a model.

MathMonkeyManlast Wednesday at 2:40 AM

One of Rob Pike's talks convinced me that the tendency to add features from other languages causes languages to all resemble each other. It's not a bad thing, but it's something to note. Consider the alternative: What if we had different languages for different tasks, new languages appearing from time to time, rather than accreting features onto existing general purpose languages every few years?

He also made the point that if you have two ways of coding something, then you have to choose every time. I've noticed that people disagree about which way is best. If there were only one way, then all of the same problems would be solved with the same amount of effort, but without any of the disagreement or personal deliberation.

Maybe Go should have exceptions beyond what panic/recover became, or maybe there should be a "?" operator, or maybe there should be a "check" expression, or some other new syntax.

Or maybe Go code should be filled with "if" statements that just return the error if it's not nil.

I've worked with a fair amount of Go code, but not enough that I am too bothered by which pattern is chosen.

On the other hand, if you spend more than a few weeks full time reading and editing Go code, you could easily learn a lot of syntax without issue. If you spend most of your career writing in a language, then you become familiar with the historical vogues that surrounded the addition of new language features and popular libraries.

There's something to be said for freezing the core language.

show 1 reply
anttiharjulast Tuesday at 6:44 PM

I was actually bit scared, so read through the whole post and am happy with the conclusion.

Go is very readable in my experience. I'd like to keep it that way.

Bratmonlast Tuesday at 7:12 PM

Watching go people complaining about how other languages encourage bubbling errors up is always hilarious to me because there is literally nothing you can do with errors in go except bubble them up, log them, or swallow them.

Even the article considers "handling" an error to be synonymous with "Adding more text and bubbling it up"!

show 3 replies
ivanjermakovlast Tuesday at 10:36 PM

No one here mentioned Zig approach there, so I'll share: https://ziglang.org/documentation/master/#try

Zig has try syntax that expands to `expr catch |e| return e`, neatly solving a common use case of returning an error to the caller.

show 2 replies
lspearslast Wednesday at 12:13 AM

Go’s restraint in adding new language features is a real gift to its users. In contrast, Swift feels like a moving target: even on a Mac Studio I’ll occasionally fail to compile a simple project. The expanding keyword list and nonstop churn make Swift harder to learn—and even harder to keep up with.

veggierolllast Tuesday at 5:39 PM

Error handling is one of my favorite parts of Go. The haters can rip `if err != nil { return fmt.Errorf("error doing thing: %w", err) }` from my cold dead hands.

show 3 replies
whstllast Tuesday at 5:05 PM

I used to hate the repetitive nature of Go’s error handling until I was burned by bad/mysterious error messages in production.

Now my error handling is not repetitive anymore. I am in peace with Golang.

However I 100% get the complaint from the people who don’t need detailed error messages.

ziml77last Tuesday at 5:34 PM

They admit it's contrived, but this isn't very convincing

    func printSum(a, b string) error {
        x, err := strconv.Atoi(a)
        if err != nil {
            return fmt.Errorf("invalid integer: %q", a)
        }
        y, err := strconv.Atoi(b)
        if err != nil {
            return fmt.Errorf("invalid integer: %q", b)
        }
        fmt.Println("result:", x + y)
        return nil
    }
It's not adding anything that the Atoi function couldn't have reported. That's a perfect case for blindly passing an error up the stack.
show 1 reply
rollcatlast Tuesday at 5:05 PM

Snip:

    if err != nil {
        return fmt.Errorf("invalid integer: %q", a)
    }
Snip. No syntax for error handling is OK with me. Spelling out "hey I actually do need a stack trace" at every call site isn't.
show 1 reply
mparnisarilast Tuesday at 8:07 PM

I have zero complaints about Go's error handling and I'm happy with the decision!

arp242last Tuesday at 5:06 PM

> Of course, there are also valid arguments in favor of change: Lack of better error handling support remains the top complaint in our user surveys.

Looking at that survey, only 13% mentioned error handling. So that means 87% didn't mention it. So in that sense, perhaps not too much weight should be given to that?

I agree the verbosity fades into the background, but also feel something better can be done, somehow. As mentioned there's been a gazillion proposals, and some of them seem quite reasonable. This is something where the original Go design of "we only put in Go what Robert, Ken, and Rob can all agree on" would IMHO be better, because these type of discussions don't really get a whole lot better with hundreds of people from the interwebz involved. That said, I wasn't a fan of the try proposal and I'm happy it didn't make it in the language.

And to be honest in my daily Go programming, it's not that big of a deal. So it's okay.

show 1 reply
bccdeelast Tuesday at 5:46 PM

Here's a non-syntactic suggestion. Could we just patch gofmt to permit this:

    if err != nil { return nil, err; }
as a well-formatted line of go? Make it a special case somehow.

My only big problem with if err != nil is that it eats up 3 lines minimum. If we could squish it down to 1, I'd be much more content.

show 3 replies
nvlledlast Wednesday at 3:11 AM

> To be more precise, we should stop trying to solve the syntactic problem

Fixing the problem purely from a syntactic perspective avoids any unexpected semantic changes that could lead to irreconcilable disagreements, as clearly demonstrated by the infamous try proposal. Very simple syntactical changes that maps clearly to the original error handling has the advantage of being trivial to implement while also avoids having the developers needing to learn something new.

rerdaviesyesterday at 3:00 AM

It's a shame they haven't polled people who refuse to use the language. No exceptions? Maybe it's time to revisit that decision.

melodyogonnalast Tuesday at 6:16 PM

Go's error handling fade away after a month of consistent use, I would not risk backwards compatibility over this. In fact, I like the explicit error handling.

show 2 replies
zahlmanlast Tuesday at 5:22 PM

Watching the process of thinking about this from the outside, somehow reminds me of my experience on the inside of the Python community trying to figure out packaging.

ajkjklast Tuesday at 5:21 PM

Okay here's my idea, not found on the list in the article, what do you think:

You add a visualization sugar via an IDE plugin that renders if/else statements (either all of them or just error cases) as two separate columns of code --- something like

    x = foo(); 
    if (x != nil)        | else
      <happy case>       | <error case>
And then successive error cases can split further, making more columns, which it is up to the IDE to render in a useful way. Underneath the representation-sugar it's still just a bunch of annoyingly nested {} blocks, but now it's not annoying to look at. And since the sugar is supported by the language developers, everyone is using the same version and can therefore rely on other developers seeing and maintaining the readability of the code in the sugared syntax.

If the error case inside a block returns then its column just ends, but if it re-converges to the main case then you visualize that in the IDE as well. You can also maybe visualize some alternative control flows: for instance, a function that starts in a happy-path column but at all of its errors jumps over into an error column that continues execution (which in code would look like a bunch of `if (x=nil) { goto err; }` cases.

Reason for doing it this way: logical flow within a single function forms a DAG, and trying to represent it linearly is fundamentally doomed. I'm betting that it will eventually be the case that we stop trying to represent it linearly, and we may as well start talking about how to do it now. Sugar is the obvious approach because it minimizes rethinking the underlying language and allows for you to experiment with different approaches.

show 2 replies
shermantanktoplast Tuesday at 8:22 PM

"Not adding extra syntax is in line with one of Go’s design rules: do not provide multiple ways of doing the same thing"

And so any change to any existing functionality is a breaking change that invalidates all code which uses that functionality? That design rule smells like hubris on the part of Go's designers. It only works if all future changes are extensions and never amendments.

Neku42last Tuesday at 9:41 PM

Even if the decision to close the door on this sucks I think they are correct - this is not a syntax problem. Adding sugar will not fix fundamental issues w/ Go's error handling.

They need to add/fix like 5-6 different parts of the language to even begin addressing this in a meaningful way.

never_inlinelast Wednesday at 2:58 AM

> When debugging error handling code, being able to quickly add a println or have a dedicated line or source location for setting a breakpoint in a debugger is helpful.

On the flip side, you can't have exception breakpoints in Go.

latchkeylast Tuesday at 7:55 PM

Gauntlet, the CoffeeScript of golang, seems to have an interesting approach:

https://gauntletlang.gitbook.io/docs/advanced-features/try-s...

keylelast Wednesday at 12:48 AM

It saddens me that the conclusion is

      "it's been like this for this long now" 
and

      "no one could ever agree to something" 
leads to this amount of verbosity. Any of these keywords approach, `try`, `check`, or `?` would have been a good addition, if they kept the opportunity to also handle it verbosely.

The argument that LLM now auto-completes code faster than ever is an interesting one, but I'm baffled by such an experienced team making this an argument, since code is read so many more times than it is written; they clearly identify the issue of the visual complexity while hand-waving the problem that it's not an issue since LLM are present to write it - it completely disregards the fact that the code is read many more times that it is written.

Visual complexity and visual rhythms are important factors in a programming language design, I feel. Go is excruciatingly annoying to read, compared to Dart, C, or any cleaner language.

Ultimately, it's just one of those "meh, too hard, won't do" solution to the problem. They'll write another one of those posts in 5 years and continue on. Clearly these people are not there to solve user problems. Hiding behind a community for decision making is weak. Maybe they should write some real world applications themselves, which involves having these error checks every 2nd lines.

At this point I wouldn't be upset if someone forked Go, called it Go++ and fixed the silly parts.

peterashfordlast Wednesday at 2:03 AM

I like Go. I use it at work and I find it pretty pragmatic.

But I can't stand the verbosity of the error handling. It drives me nuts. I also can't stand the level of rationalising that goes on when anyone dares to point out that Go's error handling is (obviously) verbose. The community has a pigheaded attitude towards criticism.

It also grinds my gears, because I really like that Go is in most other ways, simple and low on boilerplate. It's not unusual to see functions that are 50% error handling where the error handling actually DOES NOTHING. That's just insane.

cherryteastainlast Tuesday at 6:46 PM

All they have to do is to expand generics to support generic methods so we can have monadic operations like in C++'s std::expected or Rust's Result, like

    func (r Result[T, E]) AndThen[OtherT any](func(T) Result[OtherT, E]) Result[OtherT, E] { ... }
which would enable error handling like

    sum := 0
    parseAndAdd := func(s string) (func(string)Result[int, error]) { /* returns func which parses and adds to sum */ }
    return parseAndAdd(a)().AndThen(parseAndAdd(b))
There's a reason why every other language is converging to that sort of functional setup, as it opens up possibilities such as try-transform generics for ranges.
abtinflast Tuesday at 5:27 PM

IMHO, the actual problem with go error handling isn’t the error handling at all — it’s that multiple return values aren’t a first class construct. With proper tuple handling and Go’s approach to generics, a lot of these issues would just disappear.

show 1 reply
karel-3dlast Tuesday at 8:28 PM

Hm, I would agree with this 2 years ago; but now I see how great both iterators and generics were despite my initial personal objections, so I am thinking we should give some less verbose error handling a chance. But alas

unclad5968last Tuesday at 5:27 PM

I don't use Go, but I actually like Go's error handling and I think multiple return values is a better solution than any other language I've used. So much so, I've basically adopted it in my c++ code using std::pair. Errors are a value, and the abstraction over that is unnecessary in my opinion. Rust's result type is just syntactic sugar around the multiple return value approach. I don't care for the syntactic sugar, and doing many things in few lines of code isn't valuable to me, but I suspect this is why people love rust's error handling.

show 2 replies
tmalylast Wednesday at 1:42 PM

I like that it we are forced to deal with the error. It seems to make the code more predictable in some sense.

But I do loath the littered if err != nil { return err }

maybe a better autocomplete in the IDE/editor is a good compromise.

eximiuslast Tuesday at 6:33 PM

Ah, shame. `foo := someFallibleMethod()?` would have been nice for when you don't want to handle/wrap the error.

I'm not super strongly against the constant error checking - I actually think it's good for code health to accept it as inherent complexity - but I do think some minor ergonomics would have been nice.

nilirllast Tuesday at 5:46 PM

Error handling is some of the least fun parts of writing code. In all languages.

But in any case, why so much fear of being wrong?

> we have fine-grained control over the language version via go.mod files and file-specific directives

And that may be the real truth of it: Error handling in Go just isn't ... that much of a problem to force action?

show 2 replies
skissanelast Wednesday at 2:07 AM

Another way of potentially addressing this problem: add some kind of macro facility to Go – maybe take inspiration from Rust

This could be used to solve both "syntactic support for error handling", and also various other common complaints (such as lack of first class enums), without "polluting" the core language – they'd be optional packages which nobody would have to use if they don't want to

Of course, if one of these optional packages ever became truly prevalent, you could promote it to the standard library... but that would involve far less bikeshedding, because it would be about making the de facto standard de jure... and arguably, letting people vote with their feet is a much more reliable way of establishing consensus than any online discussion could ever be

JyBlast Tuesday at 8:57 PM

This is such a great move. Faith restored in the language after the generics debacle.

show 1 reply
throwaway71271last Tuesday at 5:06 PM

Everything is fine

I dream if err, if err dreams me.

bravesoul2last Tuesday at 8:44 PM

Slowly reinventing exceptions seems to go against the spirit of Go. That is what you read is what you get.

Haskell solves this with the do notation, but the price is understanding monads. Go also aims to be easy to understand.

show 1 reply
danenanialast Tuesday at 5:30 PM

I have no problem with Go’s error handling. It’s not elegant, but it works, and that’s very much in keeping with the pragmatic spirit of Go.

I’m actually kind of surprised that it’s the top complaint among Go devs. I always thought it was more something that people who don’t use Go much complain about.

My personal pet issue is lack of strict null checks—and I’m similarly surprised this doesn’t get more discussion. It’s a way bigger problem in practice than error handling. It makes programs crash in production all the time, whereas error handling is basically just a question of syntax sugar. Please just give me a way to mark a field in a struct required so the compiler can eliminate nil dereference panics as a class of error. It’s opt-in like generics, so I don’t see why it would be controversial to anyone?

show 2 replies
sedatklast Tuesday at 9:40 PM

I'm so happy that Rust sorted this out way early in its lifetime. Error handling is so nice there.

show 1 reply

🔗 View 28 more comments