Error handling in Go

There’s been a debate raging in some corners of the internet lately about how superior Go‘s error handling is to other languages.  I am going to address some of the points made, here:

Claim 1: It’s impossible to ignore errors in Go, they are “in your face”

This is patently false.  Take this example:

fmt.Println("Hello world")

Pretty innocuous wouldn't you say?  Well let's take a look at the language documentation for fmt.Println:

// Println formats using the default formats for its operands and writes to standard output.
// Spaces are always added between operands and a newline is appended.
// It returns the number of bytes written and any write error encountered.
func Println(a ...interface{}) (n int, err error)

So Println can return an error!  Where did we check it?  Well, we didn't.  Any other claims that it's OK to ignore it in this case further strengthen my argument.

Some will say that it's a deliberate choice to ignore the error and I deserve all I get. Well, was it? I didn't even know that Println returned an error until I looked at the documentation (and who is going to do that for Println?). And that's the point, if I need to look at the documentation to see that it can return an error, then if I am using a language that raises exceptions I will have also seen its documentation about how it deals with errors.

You could even argue that an exception is superior in this case.  With Go, the code will march on regardless, oblivious to the fact that Println failed.  With exceptions, it'll fail and show you exactly where it failed.

The language will error at compile time if you try to ignore an error returned as a second value and you only take the first.  But this is trivially bypassed by assigning it to _, which when reading code is easily missed compared to the exception style of "catching then dropping", because Go itself encourages this style of assigning to _ with its own range statement as a deliberate way of ignoring things that the language is trying to force you to see.

So really in both cases, ignoring the error doesn't really stand out as wrong.

Here's a concrete example in Go I was recently shown:

w := bufio.NewWriter(os.Stdout)
for _, name := range ListAll(conf) {
    fmt.Fprintln(w, name)
}
w.Flush()
return

As you can see, the caller completely forgot to check the error returned from Fprintln and Flush and there would be no compiler warning about it.

Claim 2: Exceptions teach developers to not care about errors

Citing an example where someone didn't catch an exception and the code consequently blew up is really not a good example of this claim.  It's a bug, for sure and you get a full traceback of your error in the resulting exception, which is handy.  You go away and fix it quickly based on that info.

If I am in the same situation with Go and I ignore a returned error from a function, at some point (which is likely to be nowhere near the place where the error occurred) my code will blow up.  I'll have to run up the debugger to try and find out where it really occurred though.

Because unused variables in Go are a compile-time error, it's actively discouraging you from assigning the result of the function to a variable (or you can deal with it, of course).  For anyone who's not read the full documentation for a function call or missed its return value (we're all human) as I said above - you're not even going to notice that you missed it.

Based on this, I can see no difference at all that suggests one way or the other teaches developers to not care about errors.  Developers do care about errors, really, but bugs creep in however careful you are.  And when they do, I'd rather have a decent indication of where the bug is.

Other parts of error handling that I dislike

When you look at the average Go program, you will see a lot of this:

if err != nil {
  return nil, err

This is the recommended way of error handling in in Go.  But this is not error handling, it's error propagation.  In nearly all languages there will arise situations where in well-factored code you have a low-level error that you need to pass right back up to the entry point for the caller. That means you need this error propagation code in every single place where you check for errors.  There's no syntactic sugar, just the same three lines everywhere.

For me, this vastly decreases the readability of the code. This is where exceptions excel because inside my own library I can factor the bejeesus out of it into many small functions and if I need to return an error, I just catch a lower-level exception in the top-level function and return something else.  You can do this in Go with a panic(), but it seems to be discouraged.  Panic() feels almost exactly like using exceptions, only the syntax is worse. If Go's style is to encourage people to handle errors like this, it needs the sugar.

Conclusion

Many people might think that I completely hate Go's error handling from this post.  That's not strictly true - I don't hate it, I just think it can be improved.  I challenge assumptions that I see which state that Go's error handling is superior in some way, when as far as I can see it's not that different from other languages in terms of usefulness.

Go is clearly in its infancy.  Most languages will have started out with youthful enthusiasm and realised that some change was needed.  These languages are the successful ones where developers enjoy coding in it and feel productive.  I hope that Go embraces change as it matures and attracts more developers.

I welcome comments on this post - unlike some people I won't censor them or delete ones I can't argue with (unless they are outright abusive and use foul language, this is a family blog!).

About these ads

3 thoughts on “Error handling in Go

  1. I’ll note that when I was working on parsing, it was actually recommended to me to use panic()/recover inside the library. In the standard library I believe the regexp lib does it. The idea being, within a library it is okay to use panic, but your public interface should be error returns. I can see some points about exceptions and concurrency being hard to handle, vs just an error value coming through on a channel.
    The biggest thing, though, is the loss of traceback and where the original error occurred. When Foo calls Bar and Baz, and you know Foo has returned an error, but you don’t know whether it came from Bar or Baz. You’re right that it is error propagation and you lose all the nice traceback logic (even panic() gives you nice tracebacks).
    However, using panic+recover+error return, you still lose the traceback once you convert it back into an error.

  2. I don’t have much experience in Go, barely reading the mailing list.

    Yet, early on I got the feeling that using panic/recover *inside* a package was perfectly ok (not discouraged as you seem to imply).

    It is recommended to propagate cross package errors though.

    IMBW, YYMV, HTH ;)

  3. Well said.

    One of the things I like about Go, as a practice environment, is its object type system: the only dynamic polymorphism happens between an interface and its implementation. In real programming I find I only use inheritance in a few specific patterns that are best kept isolated, and Go supports most of those patterns cleanly and minimally.

    But.

    One of those patterns, which Go does not support, is error classification. That’s one place (are there others?) where a deep type hierarchy can be helpful. When you call a function written by somebody else, you may want to handle some specific kinds of errors but the function’s author can’t always predict which ones those will be, or at what granularity. I/O failures? Network I/O failures? Specifically, that one hazard where you can’t receive confirmation after committing your changes? Most of the time the caller shouldn’t care too much about the details, because errors are errors and you can’t expect too much sanity in an insane situation. But on the other hand the super-specific ones can be great for your test suite!

    A detailed type taxonomy leaves you that flexibility, without the author having to predict exactly what you’ll be interested in or what can possibly go wrong now or in the future.

    Some of that can be done by tagging. In Go I might add marker interfaces to my error implementations. Sometimes tagging is more flexible, in fact. But it doesn’t help you maintain a solid taxonomy and, as yet, there is no standard taxonomy for new errors to slot into.

    I find it an interesting problem. So far, the only support I get from Go is “if X happens then the error return will be of type Y.” The caller may make use of that knowledge, but the smallest change could break it. How many error codes in Unix mean “didn’t work this time, but please try again”? If they added one, how would it affect your code? You need a reasonable guarantee that you can categorize the errors into categories that are meaningful to you, and you can’t do it by handling them all individually.

    You probably already know this, but:
    * You don’t know what can go wrong in somebody else’s function.
    * It’s hard enough to realize everything that can go wrong in your own. Really, it is.
    * Even if you did, you can’t know how the function may change over time.
    * Your own code may not even be in your own function in the future. It moves, it changes.
    * If you can predict every sensible change: good for you! What about all the other changes?

    Of course sometimes it’s OK because your code is only for a short while. Just like income tax was in some countries, or the UK’s 70 mph speed limit… in 1965. In general though, to think you’re above these rules is Hubris, the worst sin possible to the ancient Greeks: thinking yourself equal to the gods.

    In practice all Go programmers I see just ignore this issue. You show a sample above, where in just a few lines there are at least two unchecked I/O operations. The author of that code said it’s OK because he knows exactly what kinds of errors can happen inside the standard library, and in this case, it’s impossible to get an error return. I admire his cleverness, but I also just had a run-in with one of his programs which happily returned success after a fatal error. But I don’t blame him for that small mistake, nor all the others for some selective blindness, because I can’t expect them to be equal to the gods. What really went wrong is that Go just makes it very hard to check for everything that can go wrong — because almost anything can go wrong.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s