Comment by jerf

20 hours ago

No.

I kind of want to leave it there. But that will probably be looked on disfavorably.

I've seen at least a dozen attempts. It's not like it's hard to write it out. There's maybe a couple of variants but they're all just a handful of lines. The problem is, once you have an Option in hand, you end up trading:

    val, err := whatever(...)
    if err != nil {
        // handle error
    }
    // use val

for

    val := whatever(...)
    if err, isErr := val.Error(); isErr {
        // handle error
    }
    realVal := val.Value()
    // use realVal

What you win in nominal safety, you're definitely losing in convenience.

There's also no win in trying to offer a monadic interface like

    finalVal := whatever(...).OnVal(func (val Value) opt.Option[Result] {
        // use val
    })

because that's the minimal specification of an anonymous function in Go, so it's very inconvenient. Even if that was trimmed down, nested functions are still problematic in other ways. And you still have to unpack finalVal anyhow.

Really the solution is, install golangci-lint, turn on errcheck [1], use a pre-commit hook to make it a commit failure if golangci-lint fires, and that pretty much covers the problem in practice.

One of the problems with Option/Result/etc. advocacy... not the pattern itself, the advocacy... is that it is generally are presented, implicitly or explicitly, as if the alternative is C, with its errno and the need to not just check an error value, but remember to go actively seeking out errors constantly, making it easy to forget. But by modern standards, that's completely pathological.

If we rate error handling techniques on a scale from 1 to 10 (best), C here is a 1, and standard Option is maybe an 8 or a 9. The way Go does it is maybe a 6; it is completely true that you can neglect to handle an error (see errcheck comment in previous paragraph), but it is in your face that an error is possible, and that's really most of the problem. Putting Option/Result/etc. is not always a "go from 1 to 9" result. "Go from 6 to 8" is a much less impressive proposition, and the other inconveniences that come with it in Go tend to overwhelm the gain. I use errcheck all the time, and even in the Before Times when I was writing it all by hand it really didn't fire all that often. Especially if I exclude test code. In an AI era this hardly rates at all. AI never neglects the error.

Whether it does the right thing with it, now... that's another story entirely.

Read those error handling clauses if you're writing Go with AI. I really don't like what I've seen AIs do with them by default. What I've seen out of AI has been very thoughtless. Nominally correct in some weak sense, but thoughtless.

[1]: https://golangci-lint.run/docs/linters/configuration/#errche...

I think my opinion will be even more maligned: I like Java-style checked exceptions. It forces awareness of the error and a clean way to propagate it.

  • People advocate for Go's error handling because it forces you to deal with errors.

    But in the cases I do want to catch a specific error, the signature only tells me that a function returns an error, not which type. So I do feel that returning (int, error) is strictly worse than Java's checked exceptions if you care about errors.

    • This is elegantly solved with sum types. (And arguably needs sub typing.)

      If instead of just returning (int, error) the function would return (int, parseError | outOfBoundsError) you would know that the parser function can fail on reading a number at all and on the number being to big/small to fit the type and handle then accordingly.

      Saliently, Java in a sense has supported sum types in the throws declaration and the subsequent catch statements forever. Unfortunately it has not landed in other places where you can use types so you cannot use it for returning errors. Scala 3 supports this but has tiny adoption it seems.

  • I agree; in practice, probably only the public error types, though. If it can return fmt.Errorf("…%w…") I probably only need the type of the wrapped error, not whatever type the implementation uses.

  • The thing that most annoys me in Java in that area is when a dependency throws an unchecked error that wasn’t even documented.

    Thanks so much for that! Now I have no choice but to be reactive when something fails…

If you get the ? from rust in addition to the option, then it’s suddenly a lot more convenient. It will do `if err != nil { return err; }` in a single symbol.

There’s also convenience at the returning side. You can always just return the error, and not have to care about dummy values for the other return values (which is especially annoying when changing the returned types).

That said, it might still not be worth the added complexity.

Error handling is as important as the happy path of an application. It’s not something you sweep under the rug.

The err != nil quickly turns into metrics, logs, fallback strategies, retry mechanisms, flight recording, rate limiting, updating caches, so on.

Anyone who’s trying to shorten this hasn’t maintained any actual real software.