> You claim to understand the decisions, so I’ll push you on that.

I don't know why I'm biting at this, because it's clearly a set-up.

No matter what I say here, if my explanation of their design is in any way incomplete, I'll be taken to task for that omission and held up as an example of yet another ignorant hater who clearly doesn't understand the brilliant minds of its creators. If I accurately detail most of its purpose but make a handful of minor technical errors (after all, it's been years since I stopped using it), I expect the same.

Here goes anyway.

> Why is go error handling designed the way it is? What are the intended benefits? What are the actual benefits?

Go's error handling is designed in response to the problems its authors perceive with exceptions. There are many genuinely reasonable problems one might wish to design around.

Unchecked exceptions implicitly bubbling up from any function you might call is something they wanted to avoid. They wanted to encourage handling errors as close as possible to where those errors occur. They want to force error-handling to be explicit. And they believe that error-handling code is as important—if not moreso—than the "happy path" code, and so shouldn't be tucked away out of sight.

All of these goals are reasonable. It's ultimately the execution that's turned out awful.

What are the actual benefits? Well, it's hard to argue against the explicitness but personally I wouldn't call it a benefit. Sampling random large projects in Github demonstrates that production go code is something approaching (and potentially even exceeding) 50% error-handling stanzas in practice. In making things explicit, they've swung the pendulum way too far in the opposite direction and managed to make actual program logic dramatically more difficult to decipher.

Unchecked exceptions can't implicitly bubble up through your code, it's true. But most go error handling just... explicitly bubbles those same errors up, "decorating" them with text to serve as breadcrumbs when trying to understand where an error occurred. We've simply created human exception handlers and in doing so have lost stack traces in the process. There appears to be no convention of declaring per-error structs that might help one determine what went wrong programmatically, so every error is effectively "stringly" typed and after it's been bubbled up once it's effectively impossible for a higher layer of code to understand specifics of what might have gone wrong. Was the problem with your HTTP call a network error (try again!) or a server error (fatal)? If for some reason you couldn't handle it right where it happened, you have little chance of being able to tell the difference between the two later.

The benefits they did reap have come with some pretty massive caveats. And with this design, they've brought in additional own-goals that should have been so easy to avoid but somehow weren't.

Calling a function and doing something with the value or bubbling up the error is something like 95%+ of error handling in go. Rust makes this a single character: `?`. With go, you're forced to copypasta the error-handling stanza, hiding the actual logic you're trying to accomplish in pointless administrativia.

Further, with tuple values, you get a value and an error rather than a value or an error. For a function that returns an `int, error`, you get back a real `int` along with your error! If you make a mistake and forget to actually handle the error or bubble it up, it's all to easy to use the value. Its value might be well-defined (usually the zero-value) but the semantics of that value likely aren't. Ask me how many bugs I've seen in production code where a bug in error-handling allowed meaningless zero-values to plow their way forward through happy-path logic before causing problems somewhere completely unrelated to where the original error occurred!

All of this is to say, go's designers had real, valid concerns with exceptions in mind when designing the language's error handling constructs. What they didn't seem to do was consider what problems their design would introduce. Of course, most (but not all) of these problems could have been sidestepped by having an Option/Result type like Rust (or equivalently, a Maybe/Either type like Haskell). There's even precedent in the language for "blessed" generic types like maps and slices! They could have done this, even without introducing full generics.

> A follow up, on your abstraction point: why does go eschew abstraction? Intended upside? Actual upside?

This post has already gotten too long and honestly anyone who wants to love go despite its warts isn't going to be (nor should they be) convinced by someone writing a dissertation on HN so I'll leave the rest as an exercise to others.

But put simply, the authors' insistence on simplicity at all costs have simply put the burden of complexity on everyone else. A computer is a tower of abstractions hundreds of layers deep. Go's authors' thesis is tantamount to saying that Abstraction Level 481 is "just right" and even a single additional one would claerly make things impossible to reason about.

When one considers it in the wider scope of how many layers there already are and how the language hamstrings its users' ability to make meaningful layers below it, the whole thing comes across as absurd.