Hacker News new | past | comments | ask | show | jobs | submit login

Is modern go code still 50% "if err != nil" ?

When I last used it the error handling drove me a bit mad. I get the reasoning, but the amount of identical error checking code for (almost) every single call didn't make me feel terribly happy compared to my easy life in the erlang/elixir 'let it crash' world :}






All points are like stale mantras by now, but still:

* Errors happen. All the time. And how you handle them matters. The err != nil path is not unlikely.

* If your error processing code is repetitive, you're doing it wrong. Wrap your errors. Add context.

* Errors are values. Make them useful. Again, add context.

Honestly, while a lot of people look at Go code and think “this is 50% error checking”, every time I read an under-handed piece of C or under-catched piece of JavaScript, I think to myself “where is the error checking?”


I'm with you. I get the reasoning completely.

In my case though I've been changed by elixir and erlang, have you spent any time with them?

Once you've gotten around the whole 'let it crash' concept and supervision, and _NOT_ handling errors, it's very difficult to go back.

I wish I had a "systems-level" language where I could do this stuff, but I guess I'll continue to wait..

FWIW my C ends up having probably equal amounts of error checking as my go does, somehow it feels less repetitive though (perhaps because I use a macro or two for such things in C)


I use Go professionally and like it on balance, but error handling is my least favorite part of it.

* In practice there are not that many ways of combining fallible operations. "if err != nil { return nil, err }" is the right thing 99% of the time. Occasionally you might want to perform a set of independent operations (probably in parallel) and then handle their errors as a group. It's almost never the right thing to write complex, arbitrary logic about combinations of error values, or to ignore them. Go makes the wrong things too easy and requires the right thing to be spelled out explicitly each time. Somewhat mitigated by lint rules and editor macros, respectively, but still.

* Wrapping your errors produces great messages but defeats type switches for structured error handling. This pattern makes it easy to not actually have the error handling logic that that appears to be there.

* You need to be very careful about adding context, because you need your error monitoring system (Sentry, etc) to understand what are instances of the same failure mode vs. different failure modes. I find I can't really put any more information in the Error() string than would be in a stacktrace, or the cardinality of "unique" errors blows up. You can attach information to custom error structs and report it in dedicated log fields, but then upper layers need to know how to unroll the error stack (e.g. errors.Cause()) and access those fields.


> Wrapping your errors produces great messages but defeats type switches for structured error handling. This pattern makes it easy to not actually have the error handling logic that that appears to be there.

Not true. I've been successfully using error wrapping with type switches for years. That's what unwrapping is for. Go 1.13 has functions errors.Is(error) bool and errors.As(error, interface{}) bool and also errors.Unwrap(error) error. Package github.com/pkg/errors, which I use, only has function errors.Cause(error) error, but writing your own inspection helpers is trivial.

> You need to be very careful about adding context, because you need your error monitoring system (Sentry, etc) to understand what are instances of the same failure mode vs. different failure modes. (…)

Not sure about Sentry, but Airbrake has no issues with that.


In Java I feel like most of the time it's fine to just Throw (or let throw) errors, and a parent can catch and log and move on with its life.

Go feels like making _all_ exceptions checked, whereas I usually try to only use unchecked exceptions and it seems to work out fine :P


I think that the difference between the Go way and exceptions is that exceptions are “automatic” and thus invisible. In a language with exceptions what you get up-stack is probably something like:

  FooException at /path/to/useless/file_1.x:42
  /path/to/useless/file_1.x:69
  /path/to/useless/file_2.x:108
  …
  /path/to/you/get/the/idea.x:100500
While in properly-wrapped Go code you might get something like:

  performing request "abcd-1234" on host "pickles-1":
    requesting "storage-4":
    querying database "primary":
    profile:
    not found
And yes, you can attach code location information as well. See, for example, how Rob Pike et al. do error handling in their Upspin project[1] or the popular github.com/pkg/errors module[2].

Can you do that with exceptions? Of course! The problem is that suddenly your code will start to look a lot like "if err != nil".

[1] https://commandcenter.blogspot.com/2017/12/error-handling-in...

[2] https://godoc.org/github.com/pkg/errors


> I think that the difference between the Go way and exceptions is that exceptions are “automatic” and thus invisible

That exceptions automatically take care of propagating context is a big plus.

> See, for example, how Rob Pike et al. do error handling in their Upspin project[1]

The fact that there needs to be an entire blog post just to explain how to use errors in golang properly is quite telling about how poor of a job it does. Not to mention all the hoops they have to jump to propagate useful information, all of which we already have in languages with exceptions.


Java is probably the worst language for error handling. It has checked exceptions AND unchecked.

I can't say I've ever seen a Java codebase that handles InterruptedException properly. It's so bad that most Java devs I've talked to don't even know what the right thing to do is.

And don't get me started about catching Throwable. Why make it so easy for application code to catch VirtualMachineError. I've not once seen Throwable or Error caught in a way that makes any sense.

Java seems to encourage a culture of not handling errors properly because it looks "messy". Then, these same acculturated devs try to bring that misguided sensibility about how code should look to Go and start complaining that they actually have to handle errors.


Yeah, Java botched generics by not adding exception sum types. stream.map(f).collect(…) should throw whatever f can throw, but instead f has to wrap any checked exception, which makes them nearly unusable in practice.

But the drudgery of explicitly passing every error up the stack over and over is not a good use of human beings' time. We can generate that if we must, but not actually read it.


> not adding exception sum types What's this mean?

Ideally we want something like

  class Stream<T> {
    <R, X> Stream<R> map(
      Function<? super T, ? extends R, throws X> mapper
    ) throws X
  }
and if you pass a function that throws, say, IOException and SQLException, then X=IOException|SQLException and that's what that call to #map throws.

You can sort of do it by http://natpryce.com/articles/000738.html but you have to choose one base type and you can't make stdlib support it.


Sure it works fine until you start multithreading, where handling an exception doesn't end with unwinding the stack, because there are multiple stacks. So then have to pass it as a value to some other thread's stack, and remember to catch it in every thread or else your thread will silently die. But then you're right back to where Go is: treating errors as values.

You simply have a single uncaught-exception handler and that takes care of the problem that you mention.

Instead of 500,000 if err != nil checks littering the source code and making functions unreadable.


I’m well aware of how to deal with the problems created by unchecked, stack-unwinding exceptions. The thing is, when I’m reading Go code, I can look at a call site and easily know if an error can occur and what happens to the error (including if it’s sent to another goroutine), using very simple and consistent mechanisms. The tradeoff is verbosity, but it is worthwhile verbosity IMO because it’s simple, consistent, explicit, and does not create the unnecessary coupling of checked exceptions. I appreciate this low cognitive overhead when I go back to a codebase 6 months later, or when onboarding a new hire.

At least you can't mistakingly ignore errors, unlike in a lot of golang code I've seen where errors are either ignored, or worse, silently overwritten.

Ahh, errors. A programmer's best friend or enemy or both? I may be a rookie, but I'll say this:

- A few years ago I chose to learn Python "The Hard Way"[1] because I already had experience with scripting and a little bit of C++/Java/PHP/Js. While I fell in love with the flexibility of the language — how idiosyncratic it could be made, how quickly fluency came — error handling remained a mysterious dark space to me, a "TODO" for later. [which I didn't because I didn't program much after this first real training]

- I later tried to become "Eloquent" in Javascript[2], and while I can never praise that book enough, the language's paradigm wasn't what made error handling 'click' for me. Now I see, but it took a trip far, far away. [Js isn't my problem space, and when it is I'd rather use TypeScript probably]

- And finally Go. While I haven't finished once "The Go Programming Language"[3] yet — just followed a few great online books/courses so far (notably by Jon Calhoun[4], Xoogler), and only built a few basic things — I can say this: now I get it. I mean errors. I really do.

I think these few points mattered:

- Go has error checking 'in your face' indeed and makes it useful, because passing a value makes sense, and error type is no longer abstracted as in more OOP paradigms, it doesn't feel "otherworldly" to the present code, it's right there in the imperative flow.

- Testing is well-integrated (`_test.go` files; I know, at a basic level, but that's what I expect from a 'standard' tooling). Because it's a core feature and quite easy, you tend to use it; and testing helps thinking (conceptualizing) of a 'meta' framework around error handling. As a Go programmer you quickly understand that it's clearly, totally up to you to define this "otherworldly" space where your code exits its normal 'world'.

I find it hard to phrase it for now, but I'm positive Go's implementation of errors (essentially forcing you to go through it) made me level up big time. You clearly see the "normal execution space" and the "erronous space", and how design helps you keep your code either well within the former, or graciously through the latter. It feels incredibly safe, especially as a newbie.

Also, emmet snippets. Makes writing fast, but seeing the `if err != nil { return ... }` blocks is extremely beneficial to reading code, which is what we do most, by far.

Also, people don't mention these much but things like `panic()` and `recover()`[5] help you think of and learn about your own problem in elementary terms, first principles — do we need to halt execution or can we recover from this? It's all part of a sane and safe executive thread, or so it feels.

I don't know if I'll be programming Go at my job 5 years from now, I really don't; but I'll always have Go to thank for making me understand much in programming first principles.

[1]: https://www.goodreads.com/book/show/15858137-learn-python-th...

[2]: https://www.goodreads.com/book/show/39866497-eloquent-javasc...

[3]: https://www.goodreads.com/book/show/25080953-the-go-programm...

[4]: https://www.calhoun.io/

[5]: https://blog.golang.org/defer-panic-and-recover


Best error handling is Either aka Maybe aka Result.

So in typical golang fashion, you have to manually reimplement exceptions, but in a worse way.

That's what exceptions are for.

Honestly, I wonder if code like that just doesn't use panic / resume correctly? (Granted, I only read the book on golang about 8 years ago.)


What do you mean by “code like that”?

If you mean the code that never wraps anything and just does “return err”, then it could actually be better off using panics. In fact, it's documented in Effective Go[1]. If all you return in your API is a simple opaque “shit's broken, yo” error, then it is a perfectly valid approach to use panics. As long as they are in your package's insides, and as long as you return an error on the external border of your API instead of making users handle your panics.

[1] https://golang.org/doc/effective_go.html#recover


an exception is the same thing as what Go does, with the difference that the error is just immediately returned by default, while in go you decide what to do.

This can be helpful, in C# there isn't even a good way to find "all functions that might throw Exception foo". In Java this is slightly better since functions must declare what they can throw.

So Go is more typing than C# style exceptions, but more clear as well. There are some middle grounds such as the Java exception approach, or the ? operator in Rust, which Go is thinking of doing something similar to that.


> an exception is the same thing as what Go does, with the difference that the error is just immediately returned by default, while in go you decide what to do.

Exceptions also give you a stack trace, where golang errors don't (you have to jump through verbose hoops to get something barely similar). Secondly, it's much easier to ignore errors in golang, or worse, silently overwrite them (I've seen both in golang codebases). Whereas with exceptions, the exception gets bubbled up until it gets handled, or it terminates the entire program. This approach is much safer than golang's, where it's possible to end up in a corrupt state due to its subpar error handling implementation.


I find it interesting that in many C++, Java and C# code bases I have read and worked on, someone cooks up an error type that they use to supplement exception handling. Exceptions are used to handle “truly exceptional” things like running out of memory, and statuses are used for run of the mill errors like an invalid request being made. You choose the mechanism based on which you think suits the scenario better. I think it strikes a nice compromise between constant error checking and having one big error handler higher up in the call stack

Yes, though it doesn't bother me. I also think that with the correct design you can eliminate a lot of those checks. (e.g, avoiding pointers that can cause nil / error returns)

Go 1.13 introduced new error handling mechanisms: https://blog.golang.org/go1.13-errors

True but this does not circumvent the error checking. That proposal got denied :)

> Is modern go code still 50% "if err != nil" ?

No, It has gotten 100%.




Guidelines | FAQ | Support | API | Security | Lists | Bookmarklet | Legal | Apply to YC | Contact

Search: