Hacker News new | ask | show | jobs
by Sleaker 1 day ago
And this is -exactly- one of the reasons they didn't accept or continue with a sane proposal for more ergonomic error handling. I like go, but this was the one thing that I kept feeling like I was wasting so much time dealing with.
1 comments

Presumably you are referring to https://github.com/golang/go/discussions/71460?

While the proposed slightly reduced the token count, saving the average typist approximately 1 second of time if they don't have an autocomplete editor that types it for them, it doesn't change anything about the mental model where the real time is actually spent, so is it really sane?

Worse, it is dependent on the error type, but errors are not always of the error type. Not even Go's own standard library consistently returns errors using the error type, never mind all the other crazy things you find in the wild.

That doesn't sound sane at all.

It is true that Go not having any real kind of superpositions or side channels makes stealing popular ideas from other languages impossible (it already has both well-known error handling methods that do not depend on those properties). But a sane proposal would be designed with that in mind.

Reducing syntactic noise is about legibility, not writing ease. The error boilerplate as it exists interrupts the actual logic and makes most of your code read as sad paths that might never execute.

Of course if you want something less specific to errors, we already have do-notation as an excellent example to look at. And you can't say that's more complex for users than what they did with iterators.

> The error boilerplate as it exists interrupts the actual logic and makes most of your code read as sad paths that might never execute.

Yes! You have to think through and account for edge cases. "Might not" is the same as "might." Those branches might execute.

I have been on many projects over 25 years of software development. Projects that take error handling seriously tend to be better all around. Happy-path coding is naive.

In the overwhelming majority of cases, you want to trivially send the error up the stack to be handled, so you don't want to incur the noise in the middle of your business logic (and indeed doing so obscures the times when error handling is non-trivial!). e.g. Scala handles all error cases with compile time checking (in fact it tends to take error handling much more seriously), but you don't have to write (or read) the trivial case.
> In the overwhelming majority of cases, you want to trivially send the error up the stack to be handled

Maybe so, but for JVM languages, this "trivially" includes a stack trace. Go doesn't offer that for normal errors and there was no clear path to getting it. The need for contextual clues when debugging, and the community and then stdlib settling on error-wrapping as the way to do it, makes "simply return err" not a sane default anymore.

That's just pointing out another layer of noise Go introduces. Here again in Scala, you can get stack traces for free with no extra syntax with value-based errors. There are already solutions for doing these things that have been used in anger for many years.

In a language that takes error handling seriously, you have domain types for errors, type checking for exhaustive error handling, and you get info like a stack trace out of the box.

> Go doesn't offer that for normal errors and there was no clear path to getting it.

It does offer that for normal errors if you pass those errors via exceptions. Although you're right that using exceptions for error handling is pretty weird and generally avoided in Go, much to the chagrin of Java developers.

The Upspin experiment, which eventually shaped the error additions that did make it into Go, showed a clear path to getting it in errors themselves, but it also revealed that nobody actually uses them when available. The research found that even with stack traces attached, developers didn't bother looking at them. Thus why that bit didn't make it into Go.

> In the overwhelming majority of cases, you want to trivially send the error up the stack to be handled

There is no case where you would ever trivially send the error up the stack.

First, if your function doesn't have any meaningful information to add, you need to seriously question why the function exists. It is almost certainly not adding anything of value and the function that originally produces the error can just as well be called directly. No need to trivially pass an error when the useless "middleman" function doesn't exist.

Second, even if you still decide that a function that doesn't have worthwhile information is still worthwhile having, if you pass someone else's error straight through then you become forever coupled to the implementation of someone else. If you, for example, pass a SQLite error and then later decide you want to use Postgres, now you have to emulate SQLite errors every time Postgres produces an error, bringing you back to square one while also having one hell of a confusing API. Unless you hate other programmers and are purposely trying to make their life a living hell, the only sensible approach is to produce new error values at each step so that you can maintain stable sentinels/types.

A good language solution for errors might make it look like you are trivially passing an error up the stack while using some other method to attach the necessary metadata, like we see in some other languages, but nobody has yet figured out how to make that fit into Go. It is generally agreed upon that Go would greatly benefit from this, but until someone rolls up their sleeves and solves it...

Functions exist first to give names to conceptual blocks, and should generally be defined by "level of abstraction." i.e. you give a name to some task that then perhaps calls some low level data validation or manipulation functions which can return errors. Your business process might not care which task had an error, so it just bubbles up to the higher level coordinator (e.g. a handler just returning errors to a router).

You do not need to have new wrappers at every level. That's what interfaces are for. e.g. if all you're going to do is retry up to N times and log, and you don't need special handling, you just call String() or whatever. But you still want libraries to return domain errors so that you can handle them if you need to, or for tests, etc. And you can have some layer in the middle to make like SQLError or DatabaseError.

Right I'm saying Scala does exactly that, and has libraries that e.g. propagate stack traces across fibres automatically and invisibly. There's prior art to observe. It's not some mysterious unsolved problem.

> Reducing syntactic noise is about legibility, not writing ease.

Exactly, which said proposal does nothing to improve upon. Despite the reduction in characters it looks exactly the same as the traditional way, offering no way to improve how one think about errors. That proposal was clearly thrown out there just to try and appease the "Go doesn't have error handling" crowd without any thought into what would actually make the language better. The goal may be noble, but that particular proposal was rightfully abandoned.

> The error boilerplate as it exists interrupts the actual logic

Huh? No matter what logic you throw at the problem, there is no escaping that error handling is part of the actual logic. And it is, by far, the most important piece of the logic. Engineering is all about dealing with failure modes.

I'll grant you that there is a category of problems known as scripting tasks where error states simply can mean letting the program crash and allowing the user to clean up afterwards, but Go is clearly not trying to be a scripting language.

I agree the ? proposal seems bad, which is why I didn't speak directly to it (but gave do notation as an example of a better solution that addresses the concern about special syntax for `error` specifically). Just the idea that explicit if err! = nil return err everywhere is somehow useful.

My experience with high level banking and low-level networking applications is that the default path for error handling is that you want to bubble up to central handlers the vast majority of the time. There's a reason why exceptions are a popular language feature; they're just hard to make work with fibres. With scripting you often don't want to handle errors at all. Just set -euo.