Error handling in Go

Error handling in Go is uniquely designed to be explicit in the intent. You should return possible errors from functions and check/handle those returns. This, in contrast with other programming languages, might seem verbose and unfriendly, but it isn’t really so. Let’s go over some basic examples and move on to something less trivial.

Non errors

Go actually has a concept of a non-error. It’s a language feature and not something that you could use by writing your own function. The most obvious example of it is getting a value out of a map by key.

if val, ok := data["key"]; ok {
	// the key/value in the map exists
}

When trying to get the value at the specified key, the operation returns an optional second value, a boolean which specifies if the value returned was found in the map.

func main() {
	a := map[string]string{"key": "value"}
	b := a["key"]
	c, ok := a["key"]
	d := a["foo"]
	fmt.Printf("%#v %#v %#v %#v %#v", a, b, c, d, ok)
}

This program will run without any error. As you can see, assigning the second return value to a variable is completely optional.

Another example would be getting the information that a read from a channel was a success. In the same way, you can set a second return value to a variable when reading from a channel:

if j, ok := <-jobs; ok {
	fmt.Println("received job", j)
} else {
	fmt.Println("received all jobs")
	done <- true
}

While the second parameter is a boolean, it does denote a success or failure of a language construct and not a strict return type. You could write a func() (interface{}, bool) that would have the same semantics as the above examples, but you couldn’t skip that bool in assignment, while you can omit it from the above examples if you wanted to.

Ignoring errors

Go actually has a facility where you can ignore specific error returns. Let’s say that you want to convert a string to an integer, and you don’t really care if that conversion fails and returns a 0. You can use the _ character to ignore a specific return of that function, namely the error which you don’t need to handle in this case:

v := "abc"
s, _ := strconv.Atoi(v)
fmt.Printf("%d\n", s)

While obviously the conversion doesn’t succeed, handling the “invalid syntax” error returned in this case might be completely redundant. While this may depend on your use case, there are examples in the wild, which don’t have a real utility behind the returned error.

An example I came across a while ago is sony/sonyflake. The project is a unique ID generator which returns an int64 id and a possible error.

In order to get a new unique ID, you just have to call the method NextID.

func (sf *Sonyflake) NextID() (uint64, error)

NextID can continue to generate IDs for about 174 years from StartTime. But after the Sonyflake time is over the limit, NextID returns an error.

I’m pretty sure that nobody reading this will live 174 years from now. Do you really need to check for that specific error in this case? Should an error even be returned here?

I would consider this a design flaw, where using another facility of Go would be more appropriate: panic. To quote the excellent go by example:

A common use of panic is to abort if a function returns an error value that we don’t know how to (or want to) handle.

There are however many other examples where ignoring errors is useful. Possibly the error return that would get ignored most often is the one from json.Marshal. It takes some wisdom to know that some errors don’t need to be handled in the first place.

Sequential error handling

Your goal should be to handle all errors explicitly as they occur. That being said, it’s really easy to end up with code like this:

base64decoder := base64.NewDecoder(base64.StdEncoding, r.Body)
gz, err := zlib.NewReader(base64decoder)
if err != nil {
        return err
}
defer gz.Close()

decoder := json.NewDecoder(gz)
var t SentryV6Notice
err = decoder.Decode(&t)
if err != nil {
	return err
}
r.Body.Close()
// ...

Wouldn’t it be nice, if you could evaluate individual error returns like you would evaluate an if clause? For those of you that didn’t know:

if func1() || func2() || func3() {

This if clause would evaluate each expression individually. That means if func1() would return false, the func2 and func3 functions will never be invoked. The if clause has the capability to break execution flow - however, it doesn’t really have a way to check error returns while still using one. At best, you can do something like this:

if gz, err := zlib.NewReader(base64decoder); err != nil {
        return err
}
// ...
if err := decoder.Decode(&t); err != nil {
	return err
}

In this case, we prefixed the expression with a simple statement, which executes before the expression is evaluated. This is another feature of the Go language specification. Unfortunately, we can’t use this to control program flow. But we can consider creating a variadic function that would take func() error arguments and return as soon as the first error would be returned.

func flow(fns ...func() error) error {
	for _, fn := range fns {
		if err := fn(); err != nil {
			return err
		}
	}
	return nil
}

This playground example shows that you can implement a sequential execution flow, but it does require modifying your code in a way that significantly changes the structure of your functions. Possibly. It really depends how you store any possible function results apart from the returned errors.

If you’re dealing with jmoiron/sqlx, you can write something like this:

err := flow(
	func() error { return db.Get(result, "select one row") },
	func() error { return db.Select(result, "select multiple rows") },
	func() error { return db.Get(result, "select another row") },
)

The obvious issue is the verbosity added by wrapping the scoped functions into the function signature. If the language semantics would allow (multiple) assignment and evaluations in an if clause, the error checks might be as simple as:

var err error
if err = db.Get(result, "select one row") ||
   err = db.Select(result, "select multiple rows") ||
   err = db.Get(result, "select another row") {
	return err
)

Unfortunately, Go doesn’t evaluate errors as boolean expressions, neither does it allow assignment in a boolean expression, erroring out with “expected boolean expression, found simple statement (missing parentheses around composite literal?)”. I don’t really hold a strong opinion that this is a bad thing, because there are ways to work around it. If you ever assigned a value to a variable instead of checking for a value in some other languages like Node or PHP, you know that Go elegantly lets you work around this, sometimes very painful, caveat.

Improving error handling for your use case

Typically, API endpoints which I write include the input and output struct, a validation function, and a processing function that have the func() error signature. In a more elaborate example, I created a response writer that takes an interface{} and will write the first non-nil value or function return to the http.ResponseWriter.

// JSON responds with the first non-nil payload, formats error messages
func JSON(w http.ResponseWriter, responses ...interface{}) {
        respond := func(payload interface{}) {
                json, err := json.Marshal(payload)
                if err != nil {
                        http.Error(w, err.Error(), http.StatusInternalServerError)
                        return
                }
                w.Header().Set("Content-Type", "application/json")
                w.Write(json)
        }

        for _, response := range responses {
                switch value := response.(type) {
                case nil:
                        continue
                case func() error:
                        err := value()
                        if err == nil {
                                continue
                        }
                        respond(Error(err))
                case error:
                        respond(Error(value))
                default:
                        respond(struct {
                                Response interface{} `json:"response"`
                        }{response})
                }
                // Exit on the first output...
                break
        }
}

The benefit of such a function is that it provides conditional execution based on the variadic parameters supplied. Unlike passing a ...error or []error, you don’t need to run all the functions first.

A simple API call that would use this looks something like:

input := RequestInput{}
result := RequestResult{}
validate := func() error {
	// write and validate things for input
}
process := func() error {
	// write things to result, return error if any
}
resputil.JSON(w, validate, process, result)

In the terms of resputil.JSON at the end, the program flow is obvious and explicit, while allowing to “break out” at any time when an error would occur.

Another slight cognitive benefit is what whenever you obtain an error, you just pass that error along to the function return. You don’t need to concern yourself with other values that should be returned at this time, because it will be handled outside of the closure, only once.

Note: The language could allow a return err for functions that return more than just the error. All other return values would return their uninitialized value if you’d use this notation, thereby not violating the expected function return values. Something like it was suggested in this issue filed for Go2.

Asynchronous error handling

There was a discussion on reddit a few weeks ago where @ligustah recommended to look at the x/sync/errgroup package. The package provides a Group structure that works similarly to sync.WaitGroup, but that will return the first error that occured, or nil if none of the functions return an error. Individual functions will be executed in goroutines.

From the godoc page example “JustErrors“:

var g errgroup.Group
var urls = []string{
    "http://www.golang.org/",
    "http://www.google.com/",
    "http://www.somestupidname.com/",
}
for _, url := range urls {
    // Launch a goroutine to fetch the URL.
    url := url // https://golang.org/doc/faq#closures_and_goroutines
    g.Go(func() error {
        // Fetch the URL.
        resp, err := http.Get(url)
        if err == nil {
            resp.Body.Close()
        }
        return err
    })
}
// Wait for all HTTP fetches to complete.
if err := g.Wait(); err == nil {
    fmt.Println("Successfully fetched all URLs.")
}

First off, I think the package is great, but it does have some caveats.

  1. You might want to have all errors returned (you only get the first),
  2. You might want to break execution on error (all goroutines must finish),
  3. It launches all the checks in parallel (no sequential API included)

Depending on your use cases, you’ll quickly break out of this mold and roll a bit of your own implementation. The package itself is trivial, and as you saw in the previous section, writing a smarty pants error return checker can be only a few lines of code.

Final notes

Sometimes it seems that checking for errors in Go is necessarily a painful procedure, especially when your thinking is tainted from languages like Java or PHP or anything else which has a try/catch clause to control error flow. While you might not like it at first glance and the utility of checking for errors could be better (less verbose?), I believe that there are far worse examples of even more verbose error handling and lack thereof in those other languages.

There’s a difference between errors and panics in the fact that panics also include a program stack trace. If you want to add a stack trace to errors, I’d recommend you use the pkg/errors package by Dave Cheney. The article “Don’t just check errors, handle them gracefully” should be on a mandatory reading list for everybody.

There are significant pain points related to error handling in other languages, and if Go is a little bit verbose about it - that’s the lesser evil by far.

While I have you here...

It would be great if you buy one of my books:

I promise you'll learn a lot more if you buy one. Buying a copy supports me writing more about similar topics. Say thank you and buy my books.

Feel free to send me an email if you want to book my time for consultancy/freelance services. I'm great at APIs, Go, Docker, VueJS and scaling services, among many other things.