Go 1.13 introduces an enhanced package errors (née xerrors) which roughly standardizes programming with errors. Personally, I find the API confusing. This is a quick reference for how to use it effectively.
Sentinel errors work the same as before. Name them as ErrXxx, and create them with errors.New.
var ErrFoo = errors.New("foo error")
Error types basically work the same as before. Name them as XxxError, and make sure they have an Error method, to satisfy the error interface.
type BarError struct {
Reason string
}
func (e BarError) Error() string {
return fmt.Sprintf("bar error: %s", e.Reason)
}
If your error type wraps another error, provide an Unwrap method.
type BazError struct {
Reason string
Inner error
}
func (e BazError) Error() string {
if e.Inner != nil {
return fmt.Sprintf("baz error: %s: %v", e.Reason, e.Inner)
}
return fmt.Sprintf("baz error: %s", e.Reason)
}
func (e BazError) Unwrap() error {
return e.Inner
}
By default, when you encounter an error in a function and need to return it to
the caller, wrap it with some context about what went wrong, using
fmt.Errorf and the new %w
verb.
func process(j Job) error {
result, err := preprocess(j)
if err != nil {
return fmt.Errorf("error preprocessing job: %w", err)
}
This process is called error annotation. Avoid returning un-annotated errors, because that can make it difficult for callers to understand what went wrong.
Also, consider wrapping errors via a custom error type (like BazError, above) for more sophisticated use cases.
p := getPriority()
widget, err := manufacture(p, result)
if err != nil {
return ManufacturingError{Priority: p, Error: err}
}
Most of the time, when you receive an error, you don’t care about the details. Whatever you were trying to do failed, so you either need to report the error (e.g. log it) and carry on; or, if it’s not possible to continue, annotate the error with context, and return it to the caller.
If you care about which error you received, you can check for sentinel errors with errors.Is, and for error values with errors.As.
err := f()
if errors.Is(err, ErrFoo) {
// you know you got an ErrFoo
// respond appropriately
}
var bar *BarError
if errors.As(err, &bar) {
// you know you got a BarError
// bar's fields are populated
// respond appropriately
}
errors.Is and errors.As will both try to unwrap the error, recursively, in
order to find a match. This code
demonstrates basic error wrapping and checking techniques. Look at the order
of the checks in func a()
, and then try changing the error that’s returned by
func c()
, to get an intuition about how everything works.
As the package errors docs state, prefer using
errors.Is over checking plain equality, e.g. if err == ErrFoo
; and prefer
using errors.As over plain type assertions, e.g. if e, ok := err.(MyError)
,
because the plain versions don’t perform unwrapping. If you explicitly don’t
want to allow callers to unwrap errors, provide a different formatting verb to
fmt.Errorf
, like %v
; or don’t provide an Unwrap
method on your error type.
But these cases should be rare.