A Look At Go 1.13 Errors

Anderson Queiroz
OneFootball Tech
Published in
5 min readAug 27, 2019

Go 1.13 brings additions to the error package. They come from the proposal for Go 2 error inspection. Let's see what we've got.

In Go errors are any value implementing the error interface.

// The error built-in interface type is the conventional interface for
// representing an error condition, with the nil value representing no error.
type error interface {
Error() string
}

In essence errors are strings, easy for humans to read and interpret, but hard for programs to reason about them.

Currently there are 4 common approaches to pragmatically handle errors, they are:

  • sentinel errors
  • type assertion
  • ad-hoc checks
  • substring searches

Sentinel errors

Some packages define exported error variables and return it from its functions. The sql.ErrNoRows is a common example:

package sql// ErrNoRows is returned by Scan when QueryRow doesn’t return a
// row. In such a case, QueryRow returns a placeholder *Row value that
// defers this error until a Scan.
var ErrNoRows = errors.New(“sql: no rows in result set”)

and then we compare against it:

if err == sql.ErrNoRows {
… handle the error …
}

Type assertion

Similar to sentinel errors, in this case we want to check if the returned error is form some specific type which can give us more information. We can see a good example on the os package:

type PathError struct {
Op string
Path string
Err error
}
func (e *PathError) Error() string
func (e *PathError) Timeout() bool

With type assertion we can access all extra information available on PathError

if pe, ok := err.(*os.PathError); ok {
if pe.Timeout() { ... }
...
}

Ad-hoc checks

They are helper functions which abstract away how to what a given error might be. One clear advantage is that a package can expose such methods and keep its internals for error handling private.

// IsNotExist returns a boolean indicating whether the error is known to
// report that a file or directory does not exist. It is satisfied by
// ErrNotExist as well as some syscall errors.
func IsNotExist(err error) bool
if os.IsNotExist(err) {
...
}

Substring searches

Its name says everything, the good old strings.Contains to check for something. From all, the least desirable.

if strings.Contains(err.Error(), "foo bar") {
...
}

When we want to add more context/information: Wrapping

Quite often we need to add some more specific information, such as explaining the cause of the failure. Something we cannot do with the cases above. For example, we might need to declare that a fetch operation failed due a sql error.

Wrapping is essentially creating a chain of errors enabling us to add more information while preserving the original error. It can be easily implemented with any type capable of holding an error and more information about it. Given a type such as:

type myError struct {
msg string
err error
}
func Wrap(err error, msg string, args ...interface{}) error {
return myError{
msg: fmt.Sprintf(msg, args...),
err: err,
}
}

We can easily create some methods to traverse the error chain, give us the underneath error from a wrapping error and so on.
There are some solid libraries in the Go ecosystem doing it. Here at Onefootball github.com/pkg/errors (if you don’t know it, check it ;) ) is used in most of our microservices.

However there is a drawback with such an approach, we get vendor locked to whichever library wraps the error. As the unwrapping and any other information is only accessible through the library’s API.

Go 2 error inspection proposal

It’s proposed to Go 2 the addition of a interface for unwrapping errors:

// Unwrap returns the result of calling the Unwrap method on err, if err’s
// type contains an Unwrap method returning error.
// Otherwise, Unwrap returns nil.
type Wrapper interface {
Unwrap() error
}

[I think it be called Unwrapper rather than Wrapper]

This simple interface allows any custom error to be unwrapped by any Go program, if the current wrappers implement Unwrap() we can traverse the whole error chain without having to worry about how many custom error may have been mixed together.

Even better, remember sentinel errors and type assertions? Now Go’s errors package can define standard methods for them. They are Is and As:

func Is(err, target error) bool
func As(err error, target interface{}) bool

Some more details:

package errors

// Is reports whether any error in err's chain matches target.
//
// The chain consists of err itself followed by the sequence of errors obtained by
// repeatedly calling Unwrap.
//
// An error is considered to match a target if it is equal to that target or if
// it implements a method Is(error) bool such that Is(target) returns true.
func Is(err, target error) bool

// As finds the first error in err's chain that matches target, and if so, sets
// target to that error value and returns true.
//
// An error matches target if the error's concrete value is assignable to the value
// pointed to by target, or if the error has a method As(interface{}) bool such that
// As(target) returns true. In the latter case, the As method is responsible for
// setting target.
//
// As will panic if target is not a non-nil pointer to either a type that implements
// error, or to any interface type. As returns false if err is nil.
func As(err error, target interface{}) bool

And go 1.13?

Go 1.13 define the Unwrap, Is and As functions shown above.

Unwrap is a shorthand to call Unwrap() on something of type error. As neither of this methods were added to the error interface, errors.Unwrap is quite handy.

Is and As will match or type assert and cast any error to the target, traversing the error chain until to find a matching error or nil.

As you might have noticed, no interface was defined on Go 1.13, all three methods above check dynamically if the given error implement any of them:

u, ok := err.(interface { Unwrap() error })

x, ok := err.(interface { Is(error) bool })

x, ok := err.(interface { As(interface{}) bool })

And to wrap an error? Don’t worry, fmt has you covered:

The Errorf function has a new verb, %w, whose operand must be an error. The error returned from Errorf will have an Unwrapmethod which returns the operand of %w.

err := errors.New(“my error”)
err = fmt.Errorf(“1s wrapping my error with Errorf: %w”, err)
err = fmt.Errorf(“2nd wrapping my error with Errorf: %w”, err)

Migrating to Go 1.13? One last tip

Watch out one thing: modules now defaults to use Go module mirror and Go checksum database run by Google. TL;DR the go command will request modules to Go module mirror and checksum to Go checksum database. Therefore you need to exclude your private repos from this flow. Set GOPRIVATE to a comma-separated list of glob patterns (in the syntax of Go’s path.Match) of module path prefixes. For example:

GOPRIVATE=github.com/myOrg/*,*.corp.example.com,domain.io/private

Happy coding!

PS: I also presented a talk on this at Berlin Golang meetup, you can find the slides and code here: https://github.com/AndersonQ/go1_13_errors

--

--