Hacker News new | ask | show | jobs
by dewey 1652 days ago
I've never seen a person who writes Go for more than a few weeks complain about the error handling. I certainly don't mind it myself. Is it really a problem people have or just somewhat of a meme at this point?
5 comments

Hi, I'm one of these people.

I used Go for about six months and eventually abandoned it to pursue Rust, a decision I've been extremely satisfied with. The longer I used Go, the more I grew to hate it and error handling was one component of that.

Well over half of Go source code in practice is dealing with errors, and somehow the Go ecosystem has convinced themselves that "verbose" is the same as "explicit" when it doesn't need to be. The worst problem isn't that it's just a lot of excess code, it's that it makes all sorts of very simple and common programming tasks ridiculously unwieldy. The most obvious example is calling a fallible method, doing something to the result, and returning it (or the error). This is one single character in Rust but a minimum of four lines—with branching—of copy-pasted boilerplate in Go. Which isn't a lot in the abstract, but then you multiply that by hundreds of times and now I have read, lex, parse, and mentally discard the majority of pages of source code that's doing something that could be done in ten lines with a massive incerase of clarity in a more reasonable language.

You've probably "never seen" us because we felt very let down by the overpromise and underdelivery of go and we left.

> This is one single character

So you're bubbling up the error without annotating it... great

There's so much wrong in just this once sentence it's going to take a surprising amount of text to cover it all.

First, if you want to add extra annotations or scope to the error, you can actually do so—and trivially—while still using that single `?`. Widely-used error crates like `thiserror` allow you to specify that (for example) an I/O error will be automatically wrapped with `?` by some custom error type specific to your crate that conveys more information about what went wrong. This is phenomenal for errors that need to be bubbled up to end-users.

Second, for the majority of errors that are normal, expected, and recoverable, annotating them is just pointless busywork since they'll never be visible from outside of your program. For example, errors that eventually bubble up to an `.ok_or(...)` receive zero benefit from being annotated.

Third, is your preferred alternative the Go approach where you function as a less-capable human exception handler? Having to hunt through the source to identify what actually happened through some contortionist `error: thing went wrong: subsystem died: api client failed: gcloud client: cache error: filesystem error: file not found: tmp.VRVcBX1j` with no line numbers or function names, and various random components of the error string coming from either third-party libraries or the golang standard library? This is just so comically terrible to anyone who's spent time in languages with decent error handling it's genuinely hard to believe that people regularly come to its defense.

But of course I'm being generous here when we both know the actual status quo in the overwhelming majority of production Go projects is to simply bubble up the error with `return nil, err` with no context whatsoever, so you just get `error: file not found: tmp.VRVcBX1j` with absolutely no idea of where it came from. Those are always my favorite.

So, to recap: with Rust's `?` operator you actually can have your cake and eat it too. You can add library-specific context to your errors while actually wrapping the underlying error and not merely mashing strings together. You can opt into stack traces for your own code if you want to. And you can skip the annotations for code where you handle errors and don't bubble them up. The only apparent downside is that it's not overly verbose enough for Go adherents.

My biggest complaint about go error handling is that it's impossible to enumerate all of the errors that a function has returned. I have a use case to translate these into user facing errors for external use and find it a nightmare to enumerate them all.
What do you mean by "enumerate?" The error interface has exactly one member of type string. If a function is returning an error to you, it's specifying its own human readable error message. What's wrong with log.Fatal(err)?
I'll offer a comparison to Rust here to contrast.

In Rust, errors are generally either a struct (to represent a single possible kind of error) or an enum of structs (to represent multiple potential underlying errors). These aren't C-style enums, they're sum types. So if your function returns a Result with an Error in it, that error is precisely one of those underlying struct types.

This has some enormous advantages. If the library author provided a way to convert their errors to a human-readable string, you can simply call that method and do so (similar to go's error interface). If they didn't or you would prefer to use your own string descriptions, the enumeration provides the complete list of possible error types to the compiler. So you can match (a.k.a. case or switch) on the error type and convert them to strings of your own choosing and guarantee statically at compile time that every possible error type is handled. You can use this same machinery to detect the error type and recover from ones you know how to handle or bubble up the ones you can't.

This is much more powerful and flexible than the Go equivalent and doesn't exactly come with much additional mental burden. With Go, the only thing you're promised is that your error type can be turned into a string, that's it. You can check that the errors are of a certain type, but there is no way to know at compile-time what all possible errors are. In fact, because most Go programs just use strings as errors directly (`fmt.Errorf("bad thing happened")`), the only types of errors you can generally detect and recover safely from are ones that happen in functions that can fail in precisely one way or functions that have only one possible way to recover from all their failure modes.

Of course, Go programs could implement error structs that you can switch on. But nobody in practice seems to do this. And even if they did, there are no compile-time guarantees that ensure you're covering every unique failure case. If the function you're calling adds a new error type, there's no way to know this other than to have an `else` that covers "everything I didn't know about".

Let's take one toy example: creating a file. This could fail because the directory doesn't exist. This could fail because we don't have permissions to write to the directory. In both of these cases maybe we want to fall back to an alternate location. Or it could fail because of something unrecoverable: your disk is full. In Rust, this is trivial to do. You can match on the error type, handle ErrorKind::NotADirectory, ErrorKind::NotFound, etc., and fallback. For anything else, bubble the error up. In Go, you get an error back. The docs promise that it's of type *PathError, but that isn't enforced by the compiler so you get to typeswitch. Even then that doesn't really help you much because fs.PathError is just

    type PathError struct {
        Op   string
        Path string
        Err  error
    }
All you get about that internal error is that it's convertible to a string. So after typeswitching, now you could theoretically switch on `error.Err.String()` to do this but now you need to figure out every possible string that is returned for the error cases you want to handle. And of course, those strings could change in future updates or new ones could be added without you ever knowing.
Go developers aren't happy unless they are taking 10 lines of code and turning it into 150.
I like Go. It's useful for the things I need it for since it compiles fast into a single binary and has networking utilities in its standard library. I was used to Rust's error handling when I started, but I liked how simple Go's design was in comparison, so I stuck with it to get a proper feel for the language.

After a while, I tried using the Goland IDE, and its static analysis tool found a dozen places where I wasn't handling errors correctly: I was calling functions that return errors (such as `io.ReadCloser.Close` or `http.ResponseWriter.Write`) without assigning their results to variables, so any errors produced by them would simply be ignored. My code was compiler-error-free, go-vet warning free, and still, I was shipping buggy code.

A few months later, I try using the golangci-lint suite of linters, and again, it found even more places where I wasn't handling errors correctly: I was assigning to `err` and then, later, re-assigning to `err` without checking if there was an error in between. My code was still compiler-error-free, go-vet warning free, and now IDE-warning free — and I was still shipping buggy code.

I don't see how anyone can see this as anything other than a big ugly wart on the face of the language. It's not because it's repetitive, it's because it's fragile. Even with code I was looking at and editing regularly, it was far too easy to get wrong. I'm going to continue using Go because it still fits my purposes well, but I'm only running it on my servers, so any mistakes I make are on my head, rather than on anybody else's.

I also don't think Go's design is really amenable to things like the Option and Result types people are writing — yes, I would never have had these problems in Rust, but code written using them in Go is clunky and looks out-of-place and doesn't feel like it's the right thing to write. I wouldn't ever use the `Optional` type in the article. But it's definitely not a solution in search of a problem. There's a huge problem.

So wait, go has handle errors by returning them, but it also doesn't force you to actually handle all return values? I thought that was the entire point of implementing error handling like that.

How are we still repeating the same mistakes C made 50 years ago?

You're sort of forced to handle them, in that if a function returns (Data, error), you need to assign the error to a variable (or do data, _ := func(), but at least that indicates you're intentionally ignoring the error), and since Go treats unused variables as a compile failure, you might as well check them properly. But there's nothing that says you must check them, no.
Ah thanks, that's kind of what I thought. I've read examples of Go but haven't written a line of it myself. the way GP described it made it sound like foo, err = bar() was merely a convention and you could do foo = bar() and drop err. If you have to add ", _" that's fine.
> I don't see how anyone can see this as anything other than a big ugly wart on the face of the language.

Would you be satisfied if the compiler forced you to check error returns?

I'd be really happy with that! Building the functionality of errcheck[1] and ineffassign[2] into the compiler — or at the very least, into govet — would go a long way to allay my worries with Go.

I think the reason they don't do this is that it's a slight (albeit a very tiny one) against Go's philosophy of errors being values, just like any other. While the `error` type is standard and used throughout Go source code, it still just has a simple three-line definition[3] and is not treated as a special case anywhere else; there is nothing stopping you from returning your own error type if you wish. A third-party linter could simply check for the `error` type specifically, but the first-party tools should not, and there's nothing like Rust's `#[must_use]` attribute that could be used instead. I respect Go's philosophy, but I feel like pragmatism must win in this case.

[1]: https://github.com/kisielk/errcheck [2]: https://github.com/gordonklaus/ineffassign [3]: https://pkg.go.dev/builtin#error

Why should we have to waste our time doing something the machine can do? I normally only care about handling errors in 5% of places. The rest of the time it's just returning them. Life's too short
It might also be self-selection that people that truly dislike the error handling simply avoid golang. I’d really be interested to see how well go generics handle the Result type.
They probably stop complaining after a few weeks because there's no use. It is what it is.
meme. its like the least annoying thing I deal with on a daily bases when programming. oh no... I have to handle an error....