Unhandled errors in Go

Ralph Ligtenberg
Travix Engineering
Published in
5 min readMar 21, 2019

Go programmers know the struggle of error handling. But what about ignoring them if you don’t care? Is that a wise thing to do?

You could decide to ignore a broken window if you don’t mind the weather. Image found on pexels.com

Even though error handling can be cumbersome in Go, there is always a reason why functions return an error. And that’s why it’s not a good idea to just ignore them.

If you ask me, it’s a good thing that JetBrains’ GoLand introduced code inspections for unhandled errors in their November release. All of a sudden, I was confronted with a lot of highlighted warnings in my IDE. When I hovered over the warning, it told me that the statement contains an unhandled error:

Interesting. Apparently I was ignoring more errors than I thought. Even worse, almost all of them had to do with I/O operations, which are quite important. But then again, error handling for writing log messages doesn’t make sense to me: if you’re already using stout for logging, then there’s no other medium to report this error to. And you don’t want to panic over failed log writes either.

Initially I started underscoring to resolve the unhandled errors. It’s better to be explicit about the ignores was my opinion. But then my code became littered with underscored statements, which is not a pretty sight if you’re doing a lot of logging for example. Surely there must be a better way to deal with unhandled errors?

A possible solution could be to encapsulate these explicit ignores in functions, for example like this:

func Encode(enc *json.Encoder, v interface{}) {
_ = enc.Encode(v)
}

Which allows us to change this code:

_ = json.NewEncoder(w).Encode(s.version)

Into this:

Encode(json.NewEncoder(w), s.version)

But still, if you’re using a lot of functions containing errors you don’t want to handle, you’ll end up with a lot of wrapper functions, which still take up resources. Also, it feels a bit hacky to just ignore errors like this by hiding it in a function.

Time to do some homework: how important is it to handle all errors? And is it acceptable to explicitly ignore errors?

It turns out that there already is a pattern for this, which can also be found in the system packages. If you don’t want to handle the error, you should throw a panic. This is typically done by wrapping the function with a “must” function, like this:

func MustEncode(enc *json.Encoder, v interface{}) {
err = enc.Encode(v)
if err != nil {
panic(err)
}
}

If you look at the code, then the intent of a “must” function should be clear: we must perform a certain action. We will throw a panic if it fails, because we don’t expect it to fail.

While doing homework I also found a proposal for a “must” operator, which should act similar to the previous function. Though the proposal is rejected, it contains some interesting ideas. But the most relevant quote it contained was this:

“My concern with “Must” would be that new developers would stop handling errors. They would just use the panic feature since it’s less typing.”

True. Since developers are lazy by nature, they would often go for the solution that involves less typing. It’s actually one of the reasons why Go’s language syntax is so compact.

Though it might be tempting to start using “must” wrapper functions containing panics, it does come with a cost. Instead of relying on the caller to handle the error, the caller is now confronted with having to handle panics to catch expected errors. But panic handling is not natural, it is exceptional. So it might not be the best solution for all situations.

The previous code example contains a good example where it made sense to use it: JSON encoding. The Encode() function takes an interface and only errors when there’s an issue with the struct. But you already know the struct, unlike the encoder, and you also know whether or not it’s possible for your struct to cause an error.

In other words: if you cannot make a unit test to replicate the error, then you’ve found a good candidate to be “musted”. Instead of having to deal with an error that will never happen in your code, you can isolate the code that should be dealing with the error and (for code coverage) create a separate test on the must function to test the panic.

Unfortunately, “musting” is not always an option. Take for example this familiar line of code:

defer resp.Body.Close()

Did you know that this function returns an error? It’s defined in the Closer interface:

type Closer interface {
Close() error
}

Interesting enough, all of the code examples I’ve seen online are implicitly ignoring this error. So it seems like we can continue ignoring this error. Just to be sure, I peeked into the http package and found this internal function:

func (r *Response) closeBody() {
if r.Body != nil {
r.Body.Close()
}
}

If the system package is ignoring the error, then it must be safe for us to do so as well. We are better off making the ignore explicit though, because we just concluded that the error is safe to ignore and we want to avoid the IDE from warning us for a potential issue.

Combining handling or ignoring errors with a defer statement requires using an anonymous function, so it would look somewhat like this:

defer func() {
_ = resp.Body.Close()
}()

That’s quite an inconvenience for just ignoring an error. As a compromise, you can decide not to use “defer” to close the body and use a regular statement instead.

deferring is like transporting your code to another place. image by Ashley McNamara

As we’ve seen in the examples there are some errors that can be safely ignored and other errors that might be worth to keep an eye on by “musting” them. And of course, there are errors that can be expected. Those still need to be handled properly.

Let’s treat errors with the respect they deserve and not blindly ignore them.

--

--

Ralph Ligtenberg
Travix Engineering

Growth leader, people coach, idea catalyst, process optimizer, Agile advocate, Rubberduck, Boyscout Rule practitioner. Intentional extravert.