Go's error handling only adds noise - or does it?

Go's error handling is often criticized as noisy. However, this criticism is often based on a misconception.

There are few aspects of Go that seem to raise criticism more often than error handling. And when I come across a discussion about that topic, I often read this:

" Go's error handling is only adding noise. You have to add three extra lines only to pass the error back to the caller." And then, inevitably, this code snippet is presented:

if err != nil {
    return err
}

Often, the above is even shown in a repeating pattern, to better show the "noise":

err := f()
if err != nil {
    return err
}

err = g()
if err != nil {
    return err
}

err = h()
if err != nil {
    return err
}

Guess what – I do agree! If error handling is done as above, it is indeed just noise, and we might be better off with exceptions.

However,

this is not how errors are supposed to be handled. This is not idiomatic Go.

The point of explicit error handling is that the function can add context to an error before returning it. And so can the calling function.

func query(db *sql.DB, qs []string) error {
    
    for i, q := range qs {
        res, err := db.Query(q)
        if err != nil {
            return fmt.Errorf("query: failed at iteration %d: %w", i, err)
        }
        // do something with res
    }
    ...
}

func processList(id int, s []string) error {
    qs := ... // build queries from strings
    db := ... // open database
    err := query(db, qs)
    if err != nil {
        fmt.Errorf("processList: Cannot process list %d: %w", id, err)
    }
    ...
}

In this code snippet, each function wraps the received error (by using Errorf() and the special formatting verb %w instead of %vand adds some context that does not exists inside the called function. db.Query() does not know about the loop index i, hence query() adds that information, and query() does not know about the list ID that failed, hence processList() includes this ID in the error message.

And when the error is finally logged somewhere, the error message consists of a string of contextual info from all call levels that can be of tremendous help for troubleshooting.

This is explicit error handling that does not just add noise.

And the special formatting verb %wadvises to wrap the full error, so that the error handler can then unwrap the error down to any level and inspect the error details as needed.


Addendum

That being said, in some situations you'll want to not wrap an error. See "Whether to Wrap" in Working with Errors in Go 1.13 - go.dev for details.

(TL;DR: wrapping an error makes it part of your API. If an error represents implementation details that are likely to change, don't wrap the full error, rather, pass on the error message only.)



Background image (the blurred one) by Pixabay

Categories: The Language