Error Handling in Go

Rob Pike Reinvented Monads

Martin Kühl

Errors are values

In his post “Errors are values”, Rob Pike, one of the original authors of Go, attends the common perception that one must repetitively type

if err != nil

in order to handle errors.

He recounts an encounter of his with another Go programmer who had some code that looked like this:[1]

_, err = fd.Write(p0)
if err != nil {
    return err
}
_, err = fd.Write(p1)
if err != nil {
    return err
}
_, err = fd.Write(p2)
if err != nil {
    return err
}
// and so on

To help solve the repetition, Rob defined a type called errWriter

type errWriter struct {
    w   io.Writer
    err error
}

with a write method that stops writing to w as soon as it hits the first error:

func (ew *errWriter) write(buf []byte) {
    if ew.err != nil {
        return
    }
    _, ew.err = ew.w.Write(buf)
}

This encapsulates the repetitive error handling and lets them simplify the above code to something like this:

ew := &errWriter{w: fd}
ew.write(p0)
ew.write(p1)
ew.write(p2)
// and so on
if ew.err != nil {
    return ew.err
}

He then notes that this pattern appears often in the Go standard library, including in the bufio.Writer class which provides the same error handling as errWriter above while satisfying the io.Writer interface.

Using it changes the example into this:

b := bufio.NewWriter(fd)
b.Write(p0)
b.Write(p1)
b.Write(p2)
// and so on
if b.Flush() != nil {
    return b.Flush()
}

Rob closes by stating:

Use the language to simplify your error handling.

Using the language

The above solution is specific to io.Writer, even though the same error handling strategy makes sense for the io.Reader type, and from the blog post we know that it is in fact repeated in bufio.Scanner and the archive/zip and net/http packages.

Go does not support parametric polymorphism (or “Generics”), but if it did we could use it to write a single implementation of this error handling pattern and reuse it for different types.

Let’s check out what that might look like.

Result

Let’s start by considering io.Writer. It is an interface with exactly one method:

type Writer interface {
    Write(p []byte) (n int, err error)
}

That method’s return type is a pair consisting of a value and an error, where in the common case that error is nil, indicating that the operation finished successfully. If an error did occur, the value may still be present (and non-zero), but we’ll ignore that case for this blog post.[2]

With a sufficiently expressive type system (and using completely made up syntax) we could express this as a type Result<A>, say, which represents the result of a computation and covers two cases:

  • we either have some value of type A if the computation was successful
  • or we have some error (of type error) if the computation failed

An implementation could look similar to this:

type Result<A> struct {
    // fields
}

func (r Result<A>) Value() A {}
func (r Result<A>) Error() error {}

This type provides a place to put the error handling strategy we’re after: From a successful Result we want to run the next “step” of our program, which may itself return a Result, but as soon as one step fails we want to stop. Let’s define a method Then for this task:

func (r Result<A>) Then(f func() Result<A>) Result<A> {}

If r contains an error, Then just returns r, otherwise it calls f and returns the result of that call, which is exactly what we wanted. So far so good.

Polishing

You may have noticed that calling Then simply discards the value of r (if it’s a successful Result). We also don’t need Then to always return a Result containing the same type of value. Let’s lift those restrictions and generalize the method:

func (r Result<A>) Then(f func(A) Result<B>) Result<B> {}

Note that f is still free to ignore its argument or to return a Result containing a value of type A[3], and that a Result containing an error satisfies Result<A> for any type A.

Using this type (and a Write method that returns it), the example code from above could look something like this:[4]

r := fd.Write(p0).Then(func(_) {
        return fd.Write(p1)
    }).Then(func(_) {
        return fd.Write(p2)
    })
// and so on
if r.Error() != nil {
    return r.Error()
}

I’ll be the first to admit that this piece of code is not elegant, but I believe that that’s due to lack of support by the language, so let’s consider where improving that could get us.

The M-Word

The method Then we defined above is well known in certain circles that practice functional programming, only those folks usually call it flatMap (or bind). That’s because the Result type is a monad. (Some other names for similar types are Result, Either, Or, Xor, or \/.)

And in languages with better support for monads we can more easily express computations using them. This is what the same piece of code looks like in Haskell:

do  write fd p0
    write fd p1
    write fd p2

Yes, this code performs the exact same error handling as our examples above. Other code that handles errors the same way will also look the same, reusing the error handling strategy defined in the result type, removing the need to wrap facades around interfaces every time we want to handle the errors they generate.[5]

In the end we accomplished exactly what is being preached for Go: We treat errors as values, and we are using our language to simplify our error handling. The difference is that we only have to do that once.

Conclusion

Two commonly perceived problems of Go are that handling errors is verbose and repetitive and that parametric polymorphism is unavailable[6].

One of the authors of Go offers a solution to one of those problems, but his advice boils down to “use monads,” and because of the other problem you cannot express this concept in Go.

This leaves us having to implement artisanal one-off monads for every interface we want to handle errors for, which I think is still as verbose and repetitive.

  1. Omitting the slice expressions because they’re irrelevant to our discussion  ↩

  2. The case is easy to model but distracts from the point of the examples.  ↩

  3. Just like regular parameters, B may be different from A but doesn’t have to be.  ↩

  4. We’re following the convention of calling unused parameters _  ↩

  5. You may think that this fixes the error handling strategy of each function by way of its return type, but there are ways to parameterize those as well.  ↩

  6. Russ Cox provides a reasonable explanation  ↩

Thumb martink hl

Martin Kühl is a senior consultant at innoQ. He is interested in programming languages, styles, and tools, and strives for clarity, simplicity, and generality.

More content

Comments

Please accept our cookie agreement to see full comments functionality. Read more