Handle Errors or Pass the Buck: Pick One

Posted by Baron Schwartz on May 6, 2014 6:10:00 AM

Pick_One

In Go, errors are types like any other, and are not “exceptional” in any way. The consequences are subtle and very beneficial. Newcomers to the language are usually struck by how verbose the error handling tends to be:

fh, err := os.Open("/does/not/exist")
if err != nil {
   // Handle the error.
}

A glance through the Go standard library, or pretty much any good Go code, will show many examples of that idiom. They all looked the same at first (why don’t they just DRY it up?), but with a bit more experience, I started to see that error handling in Go is actually minimalistic, while still being complete. In many programming languages, you can omit some error handling without realizing it, but that’s harder in Go precisely because error handling is so explicit.

The seeming repetition of error handling is not really redundancy. There are essentially two subcategories. Here’s the first, which I call pass the buck:

fh, err := os.Open("/does/not/exist")
if err != nil {
   return err
}

In this case, if there’s an error I stop and report it for the caller to handle. The other style is the show must go on:

fh, err := os.Open("/does/not/exist")
if err != nil {
   // Do something to recover
}
// Execution continues here

However, when I was new to the language, I often did some kind of middle ground, like this:

fh, err := os.Open("/does/not/exist")
if err != nil {
   log.Print(err)
   return err
}

So I’ve both taken action on the error, and passed the buck for another section of code to take action on it.

Another middle-ground mistake I’ve made is to inspect and munge the error along the way, like this:

fh, err := os.Open("/does/not/exist")
if err != nil {
   log.Printf("error opening '%s': '%s'", "/does/not/exist", err)
   return fmt.Errorf("cannot open %s: %s", "/does/not/exist", err)
}

Now my log file is likely to say something like this, assuming that the caller handles the error too:

2014/04/26 16:40:41 error opening '/does/not/exist': 'open /does/not/exist: no such file or directory'
2014/04/26 16:40:41 cannot open /does/not/exist: open /does/not/exist: no such file or directory

This redundancy comes from trying to decorate the error with extra information. Sometimes it’s appropriate to do that, but in most cases errors (especially ones generated by the stdlib) will have exactly the right amount of information already, and you don’t need to say “this was an error while trying to open a file, and here’s the file,” nor do you need to say “this error occurred while trying to open a network connection”, and so forth.

Even when the errors are handled or inspected at a distance from the origin, they tend to contain all the information needed. If not, they’re poorly designed. I haven’t seen a poorly designed one in the stdlib, but I’ve made some of my own.

An additional problem with the above code is that I’ve converted the error, which might contain type-specific properties, into a string without additional information. I’ve flattened out the error and destroyed information about it.

My experience has been that, to address some of the above patterns and mis-patterns, the following suggestions are helpful:

  • Handle errors or pass the buck, but not both. This way you work with each error once and only once, concentrating responsibility for handling that specific error in only one place in the code.
  • Generally speaking, don’t mutate the error or create a new one.

The result can be both clearer code and clearer program behavior (e.g. clearer log messages). It also helps create clear scope boundaries for errors, outside which the error does not propagate. This makes it easier to reason about the code. If the error is handled and not passed on, then the boundary is explicit. If you pass the buck, then the boundary is outside the function, and is no concern of the present scope.

I’ve found that cleaning up my error-handling eliminates a lot of redundancy in code and logic. It also means that I don’t import as many things into my packages. This helps decouple packages so they’re easier to refactor independently.

Further reading:

Pic

Recent Posts

Posts by Topic

see all