Programming with errors

2019 09 11

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.

Creating errors

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
}

Wrapping and returning errors

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}
	}

Checking errors

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.