Friday, April 11, 2014

Better error handling idioms in Go

How often have you seen, or written, Go code like this?


file, err := os.Open("someFile")
if err != nil {
    return err
}

Explicit, inline error handling is necessary since Go doesn't have exceptions. The code is sufficient for small programs, even if the error is returned from more than one level of function call. Hopefully at some point the error is logged and it's fairly easy then to guess what caused the error, especially since os.Open returns a failure helpful error string, for example:

"open someFile: No such file or directory"

However, in larger programs, this approach breaks down. There tend to be too many calls which could have returned the error and (an unbounded amount of) work has to be done to isolate the failing call.

We'd like to see a stack trace, so let's add one as soon as we detect the error.

file, err := os.Open("someFile")
if err != nil {
    var stack [4096]byte
    runtime.Stack(stack[:], false)
    log.Printf("%q\n%s\n", err, stack[:])
    return err
}

The resultant log looks something like this (at least in the playground):

2009/11/10 23:00:00 "open someFile: No such file or directory"
goroutine 1 [running]:
main.main()
 /tmpfs/gosandbox-xxx/prog.go:15 +0xe0
runtime.main()
 /tmp/sandbox/go/src/pkg/runtime/proc.c:220 +0x1c0
runtime.goexit()
 /tmp/sandbox/go/src/pkg/runtime/proc.c:1394
which is better than simply seeing the error message from os.Open.

Clearly this is too much code to write after each call, but some of the code can be moved into a custom error type. (Also 4K isn't enough to capture deep stack traces which is a shame when there is enough free memory available. Maybe there's room for improvement in the runtime package?)

An important consideration is that of "soft" errors - errors which don't appear to need diagnosing at one level of the stack, but which turn out to be more serious from the perspective of one (or more) of the callers. It will probably be too expensive to capture a stack trace every time an error is detected. But it may be sufficient for the first caller which regards the error as serious to capture the stack trace. The combination of a stack trace of this caller and a reasonably helpful error message may be good enough in most cases of soft errors.

Another consideration is logging of errors. It can be very distracting to see the same error logged over and over again. So it might be necessary to keep state in an error to record whether it has already been logged.

I'm interested to hear what error handling practices are evolving in the Go community. An early blog acknowledges the problem:
It is the error implementation's responsibility to summarize the context.
but doesn't address the difficulty of large codebases where the immediate program context isn't always sufficient to diagnose problems.

Some will argue for adding exceptions to Go, but I think that may be overkill, especially for soft errors. I like explicit error handling as it encourages good recovery logic. However, there may be room for improvement in the way the context of an error can be captured. Let's see what nice idioms are beginning to emerge...




9 comments:

Glyn said...

The sequel on Twitter.

Nate Finch said...

Roger Peppe posted on twitter about https://github.com/juju/errgo, which is a good package for tracing errors through the codebase. There's a couple things about it that are somewhat annoying, but hard to escape - one is that everywhere you use errors, you need to be aware you're using errgo, because there's no automatic stack trace taken at the beginning, there's just a message and file/line number added when you use a special method to pass the error back up the stack. Also, because the error type implementation actually encapsulates the error, it means you can't just check the error type the way you might otherwise, instead you have to get the underlying error (called "Cause" in errgo) and check that thing's type.

Still, it's a step in the right direction, and it allows you to add more context to an existing error while still having access to the original error (so like, you can add comments and tracebacks to an os.IsNotExistError).

Note that errgo is not in use in production yet, due to deadlines we're trying to meet, so it's possible there's bugs or things which are suboptimal about it right now.

Glyn said...

Thanks Nate. I'm just playing with a simpler alternative which looks like the standard error type, which is less intrusive and doesn't require everyone who "percolates" the error to add their own context.

BTW I didn't notice the license for errgo. Is it open sourced?

amattn said...

I wrote deeperror for this exact purpose. It works great, especially error chaining.

https://github.com/amattn/deeperror

Unknown said...

Nice post.

A while ago I've built a library to do something similar: http://godoc.org/github.com/divoxx/stackerr

Nate Finch said...

Errgo is open source, yes. Pretty much everything Canonical does is open source, and juju specifically. Actually, I notice we're missing a LICENSE file in that repo... I'll make sure to add one. It'll be either MIT or a modified LGPL that allows for static linking (which is canonical's standard license for Go code).

Unknown said...

It would be nice to have something like the `try!` macro in Rust. It is easier in Rust because they have variants, but a language extension should allow the same pattern in Go.

Frank Laub said...

Take a look at http://godoc.org/github.com/flaub/ergo

Frank Laub said...

Another package with similar features:

http://godoc.org/github.com/flaub/ergo

Projects

OSGi (130) Virgo (59) Eclipse (10) Equinox (9) dm Server (8) Felix (4) WebSphere (3) Aries (2) GlassFish (2) JBoss (1) Newton (1) WebLogic (1)