What’s wrong with Go’s error handling?

Peter Götz
4 min readJun 15, 2017

--

Quite a few things I think. But this isn’t a Go hater post. Quite the opposite in fact. For the last 1.5 years I’ve done most of my coding in Go. Not because I had to, but because I chose to. Go’s designers got many things right and so the language has real appeal to me. And when I saw that they’re not using exceptions, I got really excited, because I think exceptions are inherently flawed.

But here’s the thing: I’m not happy with Go’s error handling.

I do applaud the Go creators for not creating yet another language with exceptions, as exceptions lead to some terrible practices in their own right.

However, the solution as currently implemented in Go does not solve the problem either. Rob Pike’s comment:

The key lesson, however, is that errors are values and the full power of the Go programming language is available for processing them.

sounds catchy and powerful, but Go’s error handling has several problems that only become obvious in practice. Let’s see what those are:

Forgetting to handle an error (I)

It’s quite easy to assign a returned error to a local variable e and then never check that value. This is the case when e was already used further up in a function. Simple example, copying between a reader and a writer:

reader := ...
file, e := os.Open(...)
if e != nil {
// handle error
}
defer file.Close()
_, e = io.Copy(file, reader)

Not checking e in the last line of this example does not generate a compile error, because e was already used before. This is a simple example, but it’s easy to construct less obvious examples.

Re-using e is in general a dangerous thing to do here and it goes against all good lessons we’ve learned from functional languages and using immutable values.

But inventing new variable names for every error in a function in order to avoid overriding a value won’t help either (besides affecting readability). Ideally the error is only in scope while it is checked, as it is when assigning it as part of an if construct:

if file, e := os.Open(...); e != nil {
// handle error
}

But this is not ideal either, because now we cannot use file after the if-block. Declaring it explicitly before the if-block seems clumsy and verbose too.

Forgetting to handle an error (II)

The example above, shows in fact another problem of return values: when you’re not interested in the return value such as in the Copy function above (number of bytes written), or for functions that just return one value, the compiler does not warn you when you forget to assign the return value to a local variable.

I think, if the argument is, that for multi-return values the compiler does not allow unused local variables, then it would only be consequent to require always to assign return values to variables. To ignore an error, the _ could be used as is common when using multiple return values.

Error inspection

Since Go’s error handling is all done through convention, there is no standard way to inspect the actual error. Sometimes packages define their errors as public variable (so-called sentinel errors), so a simple comparison does it. In other cases, you have to type cast to a API specific error type.

When using unfamiliar APIs, this can be quite tricky to get right. Furthermore, it can easily break with a new version of alibrary without noticing. A compiler-enforced mechanism (like catch in exception land does) would help here.

Stack traces and error cause chains

I’m wondering why annotated error cause chains and stack traces are not built-in. It’s a feature that is very powerful and absolutely necessary when troubleshooting errors that bubbled up from deep down to the top of a program and get reported back to the developer. An error like

open /tmp/shna7fknd.conf: no such file or directory

doesn’t help much. But something like

open /tmp/shna7fknd.conf: no such file or directory
Loading config failed
main.LoadConfig
/Users/pego/.../main.go:12
main.main
/Users/pego/.../main.go:16

is immediately fixable.

The good news is that there is a library for it:

with a helpful blog post about it:

Still, I’m a bit puzzled that the lessons learned about this from languages using exceptions were not taken over. This concept is orthogonal to the exceptions vs. return values debate.

Errors in tests, or failing fast

There is currently no easy way to turn a returned error into a panic. There is always at least this boilerplate:

e := someFunc()
if e != nil {
panic(e)
}

This is annoying in tests or bootstrapping code which is supposed to fail fast when anything goes wrong.

It’s possible to wrap the three lines into one by extracting it. Still, it leaves noise in code, that doesn’t really care about errors beyond the “fail if anything goes wrong.”

I’m not saying that these problems can’t be worked around or be minimized by careful code writing. But the truth is, with all good intentions and so on, code is never perfect. If the language could help to avoid a few of those oversights (even if that means adding some complexity to the language), it would be a big gain.

What does all that mean? Well, obviously the language won’t change in version 1 . But I think improving error handling should be taken into consideration when discussing version 2.

Dedicated error handling control structures could simplify this and make it more secure without introducing the ugliness and verbosity of traditional try-catch-constructs.

--

--

Peter Götz

SDE at AWS. I breathe code. I'm blogging here privately, opinions are my own. https://petergoetz.dev