Error handling in Go

November 21, 2022   

You can get far with Go just parrotting if err != nil, but reading these topics will help you get byeond this.

Read: https://go.dev/blog/error-handling-and-go

Read: https://dave.cheney.net/2016/04/27/dont-just-check-errors-handle-them-gracefully

Watch: https://www.youtube.com/watch?v=lsBF58Q-DnY

The error type is an interface type that wraps any value that can describe itself as a string:

type error interface {
    Error() string
}

The errors package contains an errorString type, which you can call New() to return errorString:

// errorString is a trivial implementation of error.
type errorString struct {
    s string
}

func (e *errorString) Error() string {
    return e.s
}

// New returns an error that formats as the given text.
func New(text string) error {
    return &errorString{text}
}

The alternative to errors.New("my error") is fmt.Errorf("%s", "A printf like error").

In go 1.13 error wrapping was introduced into the std library. The fmt verb %w was added to wrap errors, that is why there is no specific errors.Wrapf() in the std lib

func main() {
  if err := foo(); err != nil {
    fmt.Println(err)
  }
}

func foo() error {
  _, err := os.Open("foo.txt")
  if err != nil {
    return fmt.Errorf("failed to open file: %w", err)
  }
  return nil
}  

The advantage to using the %w is that you can unwrap the error with errors.Unwrap()

An aside: the relationship between fmt and errors package is a little confusing, digging into the fmt package you can see that fmt.Errorf is a complex wrapper around errors.New() with special handling for the %w verb


package fmt

import (
	"errors"
	"sort"
)

// Errorf formats according to a format specifier and returns the string as a
// value that satisfies error.
//
// If the format specifier includes a %w verb with an error operand,
// the returned error will implement an Unwrap method returning the operand.
// If there is more than one %w verb, the returned error will implement an
// Unwrap method returning a []error containing all the %w operands in the
// order they appear in the arguments.
// It is invalid to supply the %w verb with an operand that does not implement
// the error interface. The %w verb is otherwise a synonym for %v.
func Errorf(format string, a ...any) error {
	p := newPrinter()
	p.wrapErrs = true
	p.doPrintf(format, a)
	s := string(p.buf)
	var err error
	switch len(p.wrappedErrs) {
	case 0:
		err = errors.New(s)
	case 1:
		w := &wrapError{msg: s}
		w.err, _ = a[p.wrappedErrs[0]].(error)
		err = w
	default:
		if p.reordered {
			sort.Ints(p.wrappedErrs)
		}
		var errs []error
		for i, argNum := range p.wrappedErrs {
			if i > 0 && p.wrappedErrs[i-1] == argNum {
				continue
			}
			if e, ok := a[argNum].(error); ok {
				errs = append(errs, e)
			}
		}
		err = &wrapErrors{s, errs}
	}
	p.free()
	return err
}

type wrapError struct {
	msg string
	err error
}

func (e *wrapError) Error() string {
	return e.msg
}

func (e *wrapError) Unwrap() error {
	return e.err
}

type wrapErrors struct {
	msg  string
	errs []error
}

func (e *wrapErrors) Error() string {
	return e.msg
}

func (e *wrapErrors) Unwrap() []error {
	return e.errs
}

There’s some confusing history here. Good detail on this is provided in this Stack Overflow Post

tldr form the StackOverflow post:

  • If you want stack traces in your errors, use github.com/pkg/errors.Errorf to wrap errors.
  • If you don’t care about stack traces, use fmt.Errorf from the standard library.
  • Never use errors.Wrapf any more. It’s for backward compatibility.

End aside…

We can use arbitrary data structures as error values for example from the json package

type SyntaxError struct {
    msg    string // description of error
    Offset int64  // error occurred after reading Offset bytes
}

func (e *SyntaxError) Error() string { return e.msg }

...

if err := dec.Decode(&val); err != nil {
    if serr, ok := err.(*json.SyntaxError); ok {
        line, col := findLine(f, serr.Offset)
        return fmt.Errorf("%s:%d:%d: %v", f.Name(), line, col, err)
    }
    return err
}

SyntaxError implements Error() string. If Decode() returns and err we can do a type assertion and see if its of type json.SyntaxError, if it is then we can do special behavoirs to print out the error.

Dave Chaney recommends against using Type assertions to decode your errors because it couples your code to the error type provided by the other package. He recommends wrapping errors instead.