Hacker News new | ask | show | jobs
by cle 2605 days ago
Go's error handling is essentially the same as Error/Either/StatusOr (returning errors as values).

Re: exceptions, there are a lot of people, myself included, who do not view exceptions as "advancements", but as setbacks. Magically and suddenly subverting the normal control flow and unwinding the stack in highly concurrent programs (as is expected in Go) is an awful way to do error handling, and you end up having to bypass that mechanism and pass the errors as values across call stacks anyway, so what are you really gaining for all the extra complexity, "implicitness", and cognitive load of doing that?

3 comments

> Go's error handling is essentially the same as Error/Either/StatusOr (returning errors as values).

Except that you cannot easily chain calls that return errors, or it isn't really that hard to accidentally ignore errors because of shadowing or overwriting the variable you're storing your errors in. Or the fact that using a union type to represent errors is a strictly superior way, both in terms of usability, as well as correctness. People should just face the fact that returning errors as a product type is a mistake.

> Magically and suddenly subverting the normal control flow and unwinding the stack in highly concurrent programs (as is expected in Go)...

You may want to see what approaches Akka or Erlang takes here. golang authors just decided to ignore established practices and use clunky approaches to problems that have already been solved.

> Except that you cannot easily chain calls that return errors, or it isn't really that hard to accidentally ignore errors because of shadowing or overwriting the variable you're storing your errors in. Or the fact that using a union type to represent errors is a strictly superior way, both in terms of usability, as well as correctness. People should just face the fact that returning errors as a product type is a mistake.

These are still essentially the same. Errors are returned as values, with no major difference in runtime semantics. Whether the language supports union types is orthogonal to that.

> You may want to see what approaches Akka or Erlang takes here. golang authors just decided to ignore established practices and use clunky approaches to problems that have already been solved.

There's not one solution, there are multiple solutions with various tradeoffs. The tradeoffs Go made are congruent with its tenets as a language that values simplicity and low cognitive overhead.

> These are still essentially the same. Errors are returned as values, with no major difference in runtime semantics. Whether the language supports union types is orthogonal to that.

It's not just about runtime semantics. If it were, then exceptions should perform better than returned error values in the non-error case (which should be the majority of the time anyway). It's also about how code gets written, and more importantly, how code is read.

> There's not one solution, there are multiple solutions with various tradeoffs. The tradeoffs Go made are congruent with its tenets as a language that values simplicity and low cognitive overhead.

Which in practice, doesn't really show. Simplicity at the language level manifests as longer, more complicated code in real designs, because real life is complicated. It's pushing the load from the language and compiler implementors on to the end user.

They're essentially the same, in a sense that if/goto is essentially the same as a loop. In practice, there's a big pragmatic difference.

And I have to say, the debate around Go error handling does remind me a fair bit of some of the arguments I've read while researching that ancient debate about structured programming - needless abstraction that we're not even sure is right, it's clearer when it's explicit, language is simpler etc.

golang just brought these arguments back, and they're ending up reinventing most of what's been done, but in a subpar way (e.g. code gen instead of generics, verbose error handling + panics instead of exceptions, etc.).
FWIW I don't think panics are a bad idea necessarily, they're just very different from errors. Errors are part of the API contract (whether enforced by the language or not). Panics are for when the contract is broken on either side, or expected invariants suddenly don't hold - the reason being that if your basic guarantees about process state are broken, you can't really guarantee that you'll be able to handle the error either, and trying to do so regardless might result in a security issue.

This distinction is growing popular in general, including languages that have exceptions (e.g. FailFast in C#) and error types (e.g. panic in Rust).

> Go's error handling is essentially the same as Error/Either/StatusOr (returning errors as values).

Not really, it's very annoying to chain calls which can error. Also, as another commenter mentioned, functions which can potentially fail should return sum types and not product types.

Compare:

    do first <- computeFirst
       second <- computeSecond
       return $ f first second
and

    first, err := computeFirst()
    if err != nil {
        return nil, err
    }
    second, err := computeSecond()
    if err != nil {
        return nil, err
    }
    return f(first, second), nil
Even without do-notation the Haskell is considerably shorter:

    computeFirst >>= (\first ->
    computeSecond >>= (\second ->
        return $ f first second))
Agreed about exceptions, though; I dislike them as well.
> Even without do-notation the Haskell is considerably shorter

This is a great example, not to your point, but to the methodology of Go. You give a verbose Go example, that I expect the vast majority of readers here could understand, even if they've never written a single line of Go, followed by a terse but syntax-heavy Haskell example that I expect relatively few could.

The Go version is 78% useless noise trying to hide

  f(computeFirst(), computeSecond())
which is what a maintainer needs to focus on. We know everything can fail, we don't need to be incessantly reminded of that.

If completely inexperienced people can read idiomatic code, that means the idioms don't capture anything tricky that had to be learned the hard way. There's no payoff for getting better with the language.

The last example is Haskell without the syntactic sugar. The first example is in Haskell how it's normally written, and it's far clearer than Go.
Apologies, I know very little Haskell - does that example mean that `computeSecond` will always be called, even if `computeFirst` failed? If it does, it's not the same as the Go code which will not call `computeSecond` in that case (which might be a requirement - who knows?)

I'm also assuming that `f first second` will return (an error monad?) if either of `first` or `second` are (an error monad?) - is that right?

In the error monad above, computeSecond will not be called if computeFirst failed. But in other monads (which have the same syntax) computeSecond MAY be called depending on the rules of composition.

return is a bit of a misnomer in Haskell -- it really means "wrap this value in the monad supplied by the context." So as other commenters have mentioned, f(...) cannot fail. Non-monadic Haskell functions never use return.

If f could fail and you indeed wanted to propagate its error if it did fail, you could write the code as

    do first <- computeFirst
       second <- computeSecond
       f first second
It will not, because code inside a do-block is sequenced. The value `first` in the line `first <-computeFirst` is a non-error value. If computeFirst fails, the value does not exist (because computeFirst must return either an error or a non-error value), and so the whole computation fails.
This version of f can't fail, so "return" wraps its value in the same monad. Normally the "do" block would end by getting a monad from f, just as the Go version would normally let f return an error.
Encoding Either as a product `(a, error)` instead of a sum `a | error` is a pretty big difference.

It becomes even sillier when you have to also signal "not found" with something like `(a, bool, error)` instead of `Found a | NotFound | Error error`