Graham King

Solvitas perambulum

Collect and handle multiple errors in Go

Summary
To handle multiple operations that might return errors in Go and aggregate those errors, you can use a custom `Errs` type to collect and return all errors at the end. In the provided approach, you create an `Errs` struct and define methods to add errors, check if the collection is empty, and convert it to a single error string. This way, you can call and handle each operation, using the `Add` method to accumulate errors. Finally, use `Ret` to return a combined error, which is nil if no errors occurred, simplifying error management without adding external dependencies. The code is available in a public gist for use and modification.

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!