go 1.13: errors.As(...)

Use return values on errors behaviours when testing with errors.As(...)

For an overview of go1.13 errors package, read the go blog post. It describes two new functions errors.Is(...) which operates like an equality check and errors.As(...) which operates like a type assertion.

The example in the blog lists:

1
2
3
4
5
6
7
var e *QueryError
if errors.As(err, &e) {
    // err is a *QueryError, and e is set to the error's value
}

// Similar to:
// if e, ok := err.(*QueryError); ok { … }

Dave Cheney describes error handling patterns in this blog post and recommends omitting constants and sentinel errors in favour of errors with behaviours.

Assert errors for behaviour, not type

1
2
3
4
5
6
7
8
9
type temporary interface {
        Temporary() bool
}

// IsTemporary returns true if err is temporary.
func IsTemporary(err error) bool {
        te, ok := err.(temporary)
        return ok && te.Temporary()
}

But we can extend this example to use the go1.13 errors package.

1
2
3
4
5
6
7
8
9
type temporary interface {
        Temporary() bool
}

// IsTemporary returns true if err is temporary.
func IsTemporary(err error) bool {
	var e temporary
	return errors.As(err, &e) && e.Temporary()
}

It might be tempting to define the temporary interface without a return type:

1
2
3
type temporary interface {
	Temporary()
}

We shouldn’t do this as it reduces our ability to reuse a single error type for multiple cases.

1
2
3
4
type MyError struct {}

func (e MyError) Temporary() {}
func (e MyError) NotFound()  {}

We will not be able to differentiate between when MyError is being used for Temporary errors and when it’s being used for NotFound errors, as the MyError type will satisfy both of those interfaces.

Including a return type in the method definitions allows us to check if the behaviour is satisfied on the error type, and if the error returned represents that behaviour.

1
2
3
4
5
6
type MyError struct {
	isTemp, isNotFound bool
}

func (e MyError) Temporary() bool { return e.isTemp     }
func (e MyError) NotFound()  bool { return e.isNotFound }

Now we can reuse the single MyError definition and specify if it represents a Temporary or NotFound error.