Graham King

Solvitas perambulum

Collect and handle multiple errors in Go

In Go, how do you run several operations that might return an error, and return those errors at the end? For example you might be stopping several services:

func stopAll() error {
    if err := stopIndexer(); err != nil {
        // save the error but continue
    }
    if err := stopAuth(); err != nil {
        // save the error but continue
    }
    if err := stopJobs(); err != nil {
        // save the error but continue
    }
    [...]
    return allTheErrors
}

There are many ways to do it. Here’s how I do it, maybe it will be useful to you:

func stopAll() error {
    var errs util.Errs // code for Errs is below
    errs.Add(stopIndexer())
    errs.Add(stopAuth())
    errs.Add(stopJobs())
    return errs.Ret()
}

You treat errs.Ret() like any other error. It’s nil if none of the operations returned an error, otherwise it contains the text of all the errors. You can use errors.Is and errors.As on it, it will report if any of the internal errors match.

Why not use one of the many packages other people wrote, or publish this as a package? I try very hard to minimize dependencies. Each dependency imposes a cognitive cost on your colleagues, it is not something that should be done lightly, or alone.

Here’s the code also as a gist on github, consider it public domain and use as you will:

package util

import (
    "errors"
    "strings"
)

// Errs is an error that collects other errors, for when you want to do
// several things and then report all of them.
type Errs struct {
    errors []error
}

func (e *Errs) Add(err error) {
    if err != nil {
        e.errors = append(e.errors, err)
    }
}

func (e *Errs) Ret() error {
    if e == nil || e.IsEmpty() {
        return nil
    }
    return e
}

func (e *Errs) IsEmpty() bool {
    return e.Len() == 0
}

func (e *Errs) Len() int {
    return len(e.errors)
}

func (e *Errs) Error() string {
    asStr := make([]string, len(e.errors))
    for i, x := range e.errors {
        asStr[i] = x.Error()
    }
    return strings.Join(asStr, ". ")
}

func (e *Errs) Is(target error) bool {
    for _, candidate := range e.errors {
        if errors.Is(candidate, target) {
            return true
        }
    }
    return false
}

func (e *Errs) As(target interface{}) bool {
    for _, candidate := range e.errors {
        if errors.As(candidate, target) {
            return true
        }
    }
    return false
}

Happy error handling!