Hacker News new | ask | show | jobs
by geodel 1036 days ago
With this another most requested feature is covered by Go. This leaves error handling, enum type which are often asked by users but are not actively being worked on for now.
2 comments

The lack of Error Handling in Go is a feature, not a bug. See here: https://go.dev/doc/faq#exceptions. I think I'd be disappointed if Try/Catch ever made their way into the language.
Error handling != exceptions.

Step one would bee sum types, so only valid value space can be represented (return value or error, but not both or neither).

Good point, a poor assumption on my part
What would be the big gain from this, over the existing approach using multiple return values?
Great, so Go has support for native stacktraces so bubbled errors don't get shadowed?

Just because Go made opinionated design decisions around their error handling a decade ago when developing the language doesn't mean that there's not practical room for improvement as the language is widely in production and shortcomings in its error handling have been found.

The number of hacks I've seen over the years to try and solve the "wait, where did this error originate" problem in Go are legion, with little standardization.

And no, using Errorf with '%w' to wrap error messages along the stack isn't exactly an elegant solution.

Even if they want to keep the core error behavior as it is for compatibility, providing a core library way of wrapping with stacktraces would be a very useful next step, particularly given the most popular package doing that previously is now unmaintained.

> providing a core library way of wrapping with stacktraces would be a very useful next step

What eventually became the standard library error wrapping proposal evolved from the work done on the Upspin project. It did include stacktraces, and believed like you that it would be useful to have them. But analysis of the data showed that nobody ever really used them in practice and, for that reason, was removed from the final proposal.

> particularly given the most popular package doing that previously is now unmaintained.

Lacking wide appeal doesn't mean there isn't a niche need, of course. However, with the standard library accepting a standard for error wrapping, which this package you speak of has been updated to be compatible with, what further maintenance would be needed, exactly? It would be more concerning if it wasn't considered finished by now. It seems the solution for niche needs is right there.

In the last few months I've realized what I desperately need: a way to wrap an error with a call stack at the point where it enters our code base. This would probably save me on average 20-30 minutes a week.

I see this all the time:

   main.go:141 error: could not transmogrify the thing: a144cd21c48
And then I literally grep the code base to find the error message. That works ~50% of the time, but the other 50%, I see this:

   main.go:141 error: not found
And then I have to spend 5-10 minutes spelunking to try to find where that error might have originated from.

But this would be amazing:

   main.go:141 error: not found callstack=...
This is such an infuriating problem. I'm convinced I'm using Go wrong, because I simply can't understand how this doesn't make it a toy language. Why the $expletive am I wasting 20-30 and more minutes per week of my life looking for the source of an error!?

Have you seen https://github.com/tomarrell/wrapcheck? It's a linter than does a fairly good job of warning when an error originates from an external package but hasn't been wrapped in your codebase to make it unique or stacktraced. It comes with https://github.com/golangci/golangci-lint and can even be made part of your in-editor LSP diagnostics.

But still, it's not perfect. And so I remain convinced that I'm misunderstanding something fundamental about the language because not being able to consistently find the source of an error is such an egregious failing for a programming language.

I find it interesting how, as soon as the word error shows up, people seemingly forget how to program.

Ignore the word error for a moment. Think about how you program in the general case, for a hypothetical type T. What is it that you do to to your T values to ensure that you don't have the same problem?

Now do that same thing when T is of the type error. There is nothing special about errors.

Its flaws or merits aside, when you have no other useful context to add to the error, that's precisely what Errorf is for.

  func bar() error {
    err := baz.Transmogrify()
    return fmt.Errorf("transmogrify: %w", err)
  }

  func foo() error {
    err := bar()
    return fmt.Errorf("bar: %w", err)
  }

  func main() {
    err := foo()
    fmt.Printf("foo: %v", err)
    // foo: bar: transmogrify: not found
  }
There's your callstack, without the cost of carrying around the actual callstack.
Nitpicking here, but I prefer a different convention.

  func bar() error {
    err := baz.Transmogrify()
    return fmt.Errorf("bar: %w", err)
  }

  func foo() error {
    err := bar()
    return fmt.Errorf("foo: %w", err)
  }

  func main() {
    err := foo()
    fmt.Printf(err)
    // foo: bar: transmogrify: not found
  }
Also, I tend to skip quite a lot of layers. The (only?) advantage of manual wrapping over stack traces is that a human can leave just 3 wrappings which are deemed sufficient for another human, while stack trace would contain 100 lines of crap.
Indeed, our code base is littered with fmt.Errorf("...: %w", err), but that only works if enough places in the code add context. Currently only about 15% of return sites do this.

And I disagree that the cost of carrying around the callstack is something to worry about. Errors are akin to exceptions in C++/Java: no happy path should rely on errors for control flow (except io.EOF, but that won't generate a call stack). They should be rare enough that any cost below about 1ms and 10k is negligible.

Not exactly -- you should only fmt.Errorf wrap errors which are non-nil.

See my sibling comment: https://news.ycombinator.com/item?id=37234455

Agreed. In fact I wrote a (very) small library to help deal with it.

https://github.com/kitd/chock

>And no, using Errorf with '%w' to wrap error messages along the stack isn't exactly an elegant solution.

I don't think anyone has ever claimed otherwise. But I do think its a pretty good solution. Whats elegance worth, anyways?

Yeah I have to agree that the Go-style error handling does actually lead to better code. At least when I write it. It makes me think through how I am going to handle error states rather than chucking it in try/except in Python and hoping nothing breaks lol.
Rust-style error handling works better though - similar to Go, but with the addition of enums such that the precise types of errors which may be encountered can be easily documented in the type system.
Agreed, I’d love if they would take inspiration from Rust and bring that to Go.
Yes - if Go had sum types (and they were idiomatically used in the standard library), it would take it from a pretty good platform to a first class one.

The library ecosystem is already excellent, and the tooling is good, lack of sum types is the single wart that makes me regret it every time I pick Go up for a project.

Try/catch/finally is in the language, its just called panic/defer/recover, where panic works like throw, every function works like try, defer works like a combination of a nonselective catch that rethrows by default and finally, and recover disables the rethrows-by-default behavior, while also being the only way to interrogate the panic to see if you should do that.
I am not demanding either of these things. Just making general comment based on observing folks.
An iterator type is being actively worked on now. After that presumably the missing data types in the standard library will be filled out (set, deque, a usable heap, whatever other algorithms). After that, who knows. Maybe native bigints?

I don’t really see the enum thing happened. Is lack of enums a real problem? Theoretically, it would be convenient, but I can’t say that I see bugs caused by its lack.

Having "type MyType int" and defining a bunch of constants isn't a great replacement for enums. Yeah, it "works," but it still lets the developer forget to check for a possible variant, or you could have an underlying int that doesn't correspond to a valid variant.

The addition of enums would move all these runtime checks to compile time.

As a workaround, a list in a DB combined with a foreign key constraint. Ugly, yes.
Enums with associated values are a very basic data modeling primitive. Writing code without them is like doing arithmetic with only the multiplication sign, not the plus sign.