Coding Codes

github.com/ucirello | @ucirello in gophers.slack.com

Catalog of error handling patterns in Go

02 Dec 2017

This post is a catalog of error handling patterns that I have collected while working with Go. I do not claim these are good or that you should use them. I report them here so as a reference source for debate if you find yourself debating error handling tactic with someone this list is a useful place that you can point to explain what you mean.

I added whenever I could find public examples. Some of these patterns I saw in closed source code bases.

Although I tried to cover as many cases as I could, but I know it is not complete, thus additions are welcome.

For a function:

func f() (interface{}, error)

a) Anti-Pattern:

Ignore the error and move on using the perhaps invalid returned value.

v, _ = f()

(playground)

Refer to Go’s wiki about Code Review Comments.

b) Multi-value return:

Capture the error, and take some action that corrects the flow of the program.

v, err := f()
if err != nil {
	// ...
}

(playground)

b.1) Multi-value return and abort

Similar to the previous case, but stop application flow, effectively treating the error as non-recoverable.

v, err := f()
if err != nil {
	// ...
	return
}

(playground)

b.2) Multiple errors

If a function can go wrong in many ways before finishing, store all errors and return them together.


func multipleF() []error {
	var errs []error

	_, err1 := f()
	if err1 != nil {
		errs = append(errs, err1)
	}

	_, err2 := f()
	if err2 != nil {
		errs = append(errs, err2)
	}

	// ...

	_, errN := f()
	if errN != nil {
		errs = append(errs, errN)
	}

	return errs
}

(playground)

Example:

  • https://godoc.org/k8s.io/apimachinery/pkg/util/errors#AggregateGoroutines

c) Success or panic

The function must succeed and errors are considered programming mistakes:

func MustF() interface{} {
	v, err := f()
	if err != nil {
		panic("invalid or impossible condition about f()")
	}
	return v
}

(playground)

Examples:

  • https://golang.org/pkg/text/template/#Must
  • https://golang.org/pkg/html/template/#Must
  • https://golang.org/pkg/regexp/#MustCompile
  • https://golang.org/pkg/regexp/#MustCompilePOSIX

d) Error as part of the state of execution

The function tracks internally the presence of an error, and halts when one is found.

type noOpOnError struct {
	err error
}

func (n *noOpOnError) action() {
	if n.err != nil {
		return
	}

	// ...
	_, err := f()
	if err != nil {
		n.err = err
	}
}

func (n *noOpOnError) Err() error {
	return n.err
}

func main() {
	noop := &noOpOnError{}
	noop.action()
	noop.action()
	// ...

	if err := noop.Err(); err != nil {
		// ...
	}
}

(playground)

Examples:

  • https://golang.org/pkg/bufio/#Scanner.Err
  • https://golang.org/pkg/database/sql/#Rows.Err

d.1) Proceed on failures and return last error

The functions executes itself and do not stop in presence of errors. Keep in memory the last one that happened and return it to caller.

type lastKnownErr struct {
	err error
}

func (n *lastKnownErr) action() {
	// overwrites previous error if present
	_, err := f()
	if err != nil {
		n.err = err
	}
}

func (n *lastKnownErr) Err() error {
	return n.err
}

func main() {
	noop := &lastKnownErr{}
	noop.action()
	noop.action()
	// ...

	if err := noop.Err(); err != nil {
		// ...
	}
}

(playground)

e) Defer the error checking until a more convenient stage.

Organize datastructures in a way that errors are return at the latest stage possible. In some circumstances it makes the user code more readable by removing if err != nil blocks.

For composition:

type child struct {
	v   interface{}
	err error
}

func (c *child) action() error {
	if c.err != nil {
		return c.err
	}

	// ...
	return nil
}

type root struct {
	err error
	*child
}

func (r *root) Err() error {
	return r.err
}

func new() *root {
	v, err := f()
	return &root{
		err: err,
		child: &child{
			v:   v,
			err: err,
		},
	}
}

func main() {
	r := new()
	if err1 := r.Err(); err1 != nil {
		// ...
	}

	err2 := new().action()
	if err2 != nil {
		// ...
	}

	// err1 and err2 are equal.
}

(playground)

f) Multiple function calls with identical interfaces

When the function has to execute multiple steps that share the same interface, run them in sequence and abort on first error.

actions := []func() (interface{}, error){
	f,
	f,
}

for _, a := range actions {
	if _, err := a(); err != nil {
		// ...
	}
}

(playground)

f.1) Multiple function calls with identical interfaces, record last known error

Similar to f), but does not abort on error.

actions := []func() (interface{}, error){
	f,
	f,
}

var lastErr error
for _, a := range actions {
	if _, err := a(); err != nil {
		lastErr = err
	}
}

if lastErr != nil {
	//...
}

(playground)

g) Wrap error with execution context


func B() (interface{}, error) {
	v, err := f()
	if err != nil {
		err = fmt.Errorf("context about this failure: %v", err)
	}
	return v, err
}

func main() {
	v, err := B()
	fmt.Println(v, err)
}

(playground)

Example

  • https://godoc.org/github.com/pkg/errors#Wrap

h) Error constructor

func fErr(msg, v interface{}) error {
	return fmt.Errorf("f() error: %v %v", msg, v)
}

func f() (interface{}, error) {
	var isRight bool
	if !isRight {
		return isRight, fErr("isRight", isRight)
	}
	return isRight, nil
}

func main() {
	_, err := f()
	fmt.Println(err)
}

(playground)

Example

  • https://godoc.org/github.com/upspin/upspin/errors#E
  • https://godoc.org/k8s.io/apimachinery/pkg/api/errors#StatusError

i) Error matching by value equality

var ErrIsWrong = errors.New("value is wrong")

func f() (interface{}, error) {
	var isRight bool
	if !isRight {
		return isRight, ErrIsWrong
	}
	return isRight, nil
}

func main() {
	_, err := f()
	if err == ErrIsWrong {
		fmt.Println("ErrIsWrong")
	}
}

(playground)

i.1) Error matching by string

var ErrIsWrong = errors.New("value is wrong")

func f() (interface{}, error) {
	var isRight bool
	if !isRight {
		return isRight, ErrIsWrong
	}
	return isRight, nil
}

func main() {
	_, err := f()
	if err != nil && strings.Contains(err.Error(), ErrIsWrong.Error()) {
		fmt.Println("ErrIsWrong")
	}
}

(playground)

i.2) Error matching by type

type SomeError struct{}

func (s *SomeError) Error() string {
	return "error message"
}

var ErrIsWrong = &SomeError{}

func a() error {
	return errors.New("other error")
}

func b() error {
	return ErrIsWrong
}

func main() {
	err1 := a()
	if _, ok := err1.(*SomeError); ok {
		fmt.Println("err1 is SomeError")
	}

	err2 := b()
	if _, ok := err2.(*SomeError); ok {
		fmt.Println("err2 is SomeError")
	}
}

(playground)