Home

Error handling in Go

Saturday 15 December 2012

I think one of the most often criticised and discussed aspects of Go is error handling. Go's explicit, uncompromising way of dealing with errors can take up a reasonable proportion of your codebase. But I regard it as a feature, one that encourages easy-to-read code.

Panic

Go has a mechanism called panic, which allows a program to get out of an untenable position. It's not meant to be used for ordinary errors, and I'll discuss it in another post.

Multiple return values

Go has the wonderful ability to return more than one value from a function. This removes the need to create function-specific structs all over the code, and doesn't copy C's horrible habit of returning error values in-band.
int atoi( const char * str );
int a = atoi(s);
if (a == 0 && s[0] != '0'){
    //handle error
}    
This function will return zero if the string (or C equivalent) cannot be parsed. It will also return zero if given "0" (or "-0" etc…). Zero seems like a fairly sensible number for it to return, but it doesn't signal whether or not the string was a valid number. If that matters to your application, you would have to check after every call. I'll ignore all the undefined behaviour that's possible in this snippet, as well as atoi's behaviour with whitespace or trailing characters.
func Atoi(s string) (i int, err error)
a, err := Atoi(s)
if err != nil {
    //handle error
}
The equivalent Go code looks pretty similar. But here we don't have to think up a way of checking if there was an error for each function, we get a second return value that explicitly tells us. This makes it much easier to write robust, correct code. It also means that we are much more aware of the possibility of errors, and may start to obsessively check every function. That's hardly a bad thing, but you may notice that you're handling more errors than in other languages.

type error interface

Virtually all errors in Go are represented by the built-in error interface, defined as anything that has an Error() string method. Occasionally you'll see boolean "ok" values, telling the caller if the operation succeeded but without giving any information. Since the error type is an interface, you can return anything you want along with your error, if it'll help the caller decide what to do next. The error type is not special, the compiler doesn't treat it any differently - it acts like any other type. The zero value for interfaces is nil, representing no error.
type state struct {
    input string
    count int
    err   string
}
func (s state) Error() string{
    return s.err
}
func Calc(input s) (int, error) {
    err := state{input,0,""}
    //Something goes wrong
    return 0, err
}
Here, the caller of Calc() will notice the error and can examine its cause. The error contains enough information to tell the caller what happened. So Go can signal both that an error occurred, and why. This is done without parsing the error string (the result of calling .Error()).
Sometimes, an error is not worth investigating. Standard procedure may be to log it, and continue or return it to the previous caller. In that case the helper functions in packages errors or fmt may be useful.
if x == 0 {
    return 0, fmt.Errorf("Cannot proceed: x = %d, y = %d", x, y)
}
Still, sometimes an error is really not worth investigating. In which case the caller can just ignore it, assigning it to the blank identifier _.
n, _ := Calc("Hello")
And if you don't want any of the return values, then you can ignore them all. In fact this is used in any "Hello World" program. fmt.Println() returns an int and an error, representing the number of bytes written and a possible error. But really... what are you going to do if you get an error whilst printing?
Calc("Hello")
fmt.Println("Hello")
This makes the possibility of errors obvious. If something is going to go wrong, you have to handle the error in some way, or take the decision to ignore it. And the code that handles the error is the caller of the function that encountered it. There is usually the best place to decide what to do. Perhaps you need to stop, perhaps you can continue, perhaps you can retry. The surrounding code probably has all the information it needs to make the decision, and is more likely to be able to handle it properly than an exception caught some distance away.

Previous (Using Package Heap)
Next (Gob encoding an interface)