Examples of Go Error Handling

Michael Stöckli
4 min readSep 1, 2017

--

I started writing Go code professionally about four months ago. Learning the language was a straight forward experience, for the most part. The main challenges that I faced were trying to write idiomatic Go code after having spent most of my career writing Java, and how to do error handling.

After having consumed some material on Go error handling, I felt like there were some parts missing in order to have a complete picture of error handling in Go. In this post, I will try to summarise the variations of error handling that I have seen so far. Most of the content comes from Dave Cheneys post, documentation for some libraries and the Go source code.

Cheney discusses three types of errors. I tried to find some examples of these and (hopefully) provide some extra data points for deciding how to do error handling in your project.

Sentinel errors. Publicly exposed errors as values that you can check for equality. The Gorm package uses a set of sentinel errors that can help you identify what error has occurred.

var q Quote
err := s.db.Table("quote").Where("author=?", "dave").First(&q).Error
if err == gorm.ErrRecordNotFound {
// handle not found ...
}

An argument against exposing errors as values is that they become a rigid part of your API. My main gripe is that the stack-trace (use fmt.Printf("%+v", err)) of this type of error will point to the init() function where the error was declared. Not a big deal in the use-case above, but quite frustrating for other errors.

ZeroMQ errors use a variation of this by allowing the caller to check for equality on error codes.

zmsg, err := c.socket.RecvMessageBytes(0)
if err != nil {
switch zmq.AsErrno(err) {
case zmq.Errno(syscall.EAGAIN):
return nil, ErrTimeout
default:
return nil, err
}
}

Typed errors. Instead of using equality as above, we switch on the type of the error. We can then query the error for more specific information. An example of this is querying the Mongo driver mgo for a network timeout.

func isTimeout(err error) bool {
switch e := err.(type) {
case net.Error:
return e.Timeout()
case *mgo.BulkError:
for _, be := range e.Cases() {
// Grab the first error only.
return isTimeout(be.Err)
}
}
return false
}

The advantage of using typed errors is that you can choose to expose a single error type that the caller can query for more specific information. You as the API designer can change what the actual method calls do under the hood without breaking the API design. How the hell do you know what error types are exposed by the package? Tracing through the source code of a package is quite time consuming, instead I try to create test failure scenarios that I expect to happen and print the error.

// Grab error, I think this should be a net.Error, but not sure.
err := sess.DB("ex").C("quotes").Find(nil).All(&out)
fmt.Printf("error: %#v", err)
// Or just the type of the error.
fmt.Printf("error type: %T", err)

The file under net/net.go has quite a bit of error related code that is worth taking a look at.

Opaque errors. The caller doesn’t know anything about the error, just that the operation failed. API wise great since the package implementors don’t have to worry about making breaking changes when there’s nothing to break in the first place. This isn’t really a step up from all the simple Go examples floating around.

A few packages implement assertion functions to find out more information about the error. Package mgo has a function for checking if the error is due to duplicate documents:

err := sess.DB("ex").C("quotes").insert(funnyQuote)
if err != nil && mgo.IsDup(err) {
// Trying to 'insert' a quote that already exists.
// Handle it gracefully.

The containerd source has an errdefs package that exposes several func IsXxx(error)bool functions. The os package in Go exposes assertions too.

Adding assertions to your package is another form of exposing an API, just like using typed errors. The advantage this method has over typed errors is that the caller doesn’t need to dig to see what errors they should expect since checking the package for IsXxx functions should suffice.

How should I handle errors in my code?

Internal error handling. There’s no general approach, but do use Cheneys errors package that allows you to annotate errors with stack information and description.

So the following

if err != nil {
return err
}

becomes

if err != nil {
return errors.WithStack(err)
}

Now the caller gets a nice stack-trace that includes all other calling functions, not just the originator. Alternatively use errors.Wrapf to add a description too.

Exposed error handling. So an error has escaped your package and the caller wants to make decision on how to handle the error. Best we expose some assertion to help the caller, e.g.

func IsTimeout(err error) bool {
if err, ok := errors.Cause(err).(net.Error); ok {
return err.Timeout()
}
return false
}

We have to use errors.Cause to find the originating error when using errors.WithStack or errors.Wrapf. Writing a handful of IsXxx functions is also nice for internal error handling.

Currently, you as the caller of a function that returns an error will need to handle all the different types of errors presented above. Creating small assertions that can be used for both internal and exposed error handling are preferable to having to handle all different types directly at the call-site.

Bullets

  • Write assertion functions to help you handle errors, like IsTimeout above, remember to use errors.Cause.
  • Expose assertion functions to help callers handle errors, e.g. quotestore.IsNotFound(error)bool for an example quotestore package.
  • Do not inspect the error.Error()string to help you handle the error. Most if not all packages supply some mechanism to help you understand the error. Use fmt.Printf("%T", err) or fmt.Printf("%#v", err) to get more information.

--

--