blob: a3aa755a21dc08edff3747d00e4387cd0df2af9b [file] [log] [blame]
Working with Errors in Go 1.13
17 Oct 2019
Tags: errors, technical
Damien Neil and Jonathan Amsterdam
* Introduction
Gos treatment of [[https://blog.golang.org/errors-are-values][errors as values]]
has served us well over the last decade. Although the standard librarys support
for errors has been minimaljust the `errors.New` and `fmt.Errorf` functions,
which produce errors that contain only a messagethe built-in `error` interface
allows Go programmers to add whatever information they desire. All it requires
is a type that implements an `Error` method:
type QueryError struct {
Query string
Err error
}
func (e *QueryError) Error() string { return e.Query + ": " + e.Err.Error() }
Error types like this one are ubiquitous, and the information they store varies
widely, from timestamps to filenames to server addresses. Often, that
information includes another, lower-level error to provide additional context.
The pattern of one error containing another is so pervasive in Go code that,
after [[https://golang.org/issue/29934][extensive discussion]], Go 1.13 added
explicit support for it. This post describes the additions to the standard
library that provide that support: three new functions in the `errors` package,
and a new formatting verb for `fmt.Errorf`.
Before describing the changes in detail, let's review how errors are examined
and constructed in previous versions of the language.
* Errors before Go 1.13
** Examining errors
Go errors are values. Programs make decisions based on those values in a few
ways. The most common is to compare an error to `nil` to see if an operation
failed.
if err != nil {
// something went wrong
}
Sometimes we compare an error to a known _sentinel_ value, to see if a specific error has occurred.
var ErrNotFound = errors.New("not found")
if err == ErrNotFound {
// something wasn't found
}
An error value may be of any type which satisfies the language-defined `error`
interface. A program can use a type assertion or type switch to view an error
value as a more specific type.
type NotFoundError struct {
Name string
}
func (e *NotFoundError) Error() string { return e.Name + ": not found" }
if e, ok := err.(*NotFoundError); ok {
// e.Name wasn't found
}
** Adding information
Frequently a function passes an error up the call stack while adding information
to it, like a brief description of what was happening when the error occurred. A
simple way to do this is to construct a new error that includes the text of the
previous one:
if err != nil {
return fmt.Errorf("decompress %v: %v", name, err)
}
Creating a new error with `fmt.Errorf` discards everything from the original
error except the text. As we saw above with `QueryError`, we may sometimes want
to define a new error type that contains the underlying error, preserving it for
inspection by code. Here is `QueryError` again:
type QueryError struct {
Query string
Err error
}
Programs can look inside a `*QueryError` value to make decisions based on the
underlying error. You'll sometimes see this referred to as "unwrapping" the
error.
if e, ok := err.(*QueryError); ok && e.Err == ErrPermission {
// query failed because of a permission problem
}
The `os.PathError` type in the standard library is another example of one error which contains another.
* Errors in Go 1.13
** The Unwrap method
Go 1.13 introduces new features to the `errors` and `fmt` standard library
packages to simplify working with errors that contain other errors. The most
significant of these is a convention rather than a change: an error which
contains another may implement an `Unwrap` method returning the underlying
error. If `e1.Unwrap()` returns `e2`, then we say that `e1` _wraps_ `e2`, and
that you can _unwrap_ `e1` to get `e2`.
Following this convention, we can give the `QueryError` type above an `Unwrap`
method that returns its contained error:
func (e *QueryError) Unwrap() error { return e.Err }
The result of unwrapping an error may itself have an `Unwrap` method; we call
the sequence of errors produced by repeated unwrapping the _error_chain_.
** Examining errors with Is and As
The Go 1.13 `errors` package includes two new functions for examining errors: `Is` and `As`.
The `errors.Is` function compares an error to a value.
// Similar to:
// if err == ErrNotFound { … }
if errors.Is(err, ErrNotFound) {
// something wasn't found
}
The `As` function tests whether an error is a specific type.
// Similar to:
// if e, ok := err.(*QueryError); ok { … }
var e *QueryError
if errors.As(err, &e) {
// err is a *QueryError, and e is set to the error's value
}
In the simplest case, the `errors.Is` function behaves like a comparison to a
sentinel error, and the `errors.As` function behaves like a type assertion. When
operating on wrapped errors, however, these functions consider all the errors in
a chain. Let's look again at the example from above of unwrapping a `QueryError`
to examine the underlying error:
if e, ok := err.(*QueryError); ok && e.Err == ErrPermission {
// query failed because of a permission problem
}
Using the `errors.Is` function, we can write this as:
if errors.Is(err, ErrPermission) {
// err, or some error that it wraps, is a permission problem
}
The `errors` package also includes a new `Unwrap` function which returns the
result of calling an error's `Unwrap` method, or `nil` when the error has no
`Unwrap` method. It is usually better to use `errors.Is` or `errors.As`,
however, since these functions will examine the entire chain in a single call.
** Wrapping errors with %w
As mentioned earlier, it is common to use the `fmt.Errorf` function to add additional information to an error.
if err != nil {
return fmt.Errorf("decompress %v: %v", name, err)
}
In Go 1.13, the `fmt.Errorf` function supports a new `%w` verb. When this verb
is present, the error returned by `fmt.Errorf` will have an `Unwrap` method
returning the argument of `%w`, which must be an error. In all other ways, `%w`
is identical to `%v`.
if err != nil {
// Return an error which unwraps to err.
return fmt.Errorf("decompress %v: %w", name, err)
}
Wrapping an error with `%w` makes it available to `errors.Is` and `errors.As`:
err := fmt.Errorf("access denied: %w", ErrPermission)
...
if errors.Is(err, ErrPermission) ...
** Whether to Wrap
When adding additional context to an error, either with `fmt.Errorf` or by
implementing a custom type, you need to decide whether the new error should wrap
the original. There is no single answer to this question; it depends on the
context in which the new error is created. Wrap an error to expose it to
callers. Do not wrap an error when doing so would expose implementation details.
As one example, imagine a `Parse` function which reads a complex data structure
from an `io.Reader`. If an error occurs, we wish to report the line and column
number at which it occurred. If the error occurs while reading from the
`io.Reader`, we will want to wrap that error to allow inspection of the
underlying problem. Since the caller provided the `io.Reader` to the function,
it makes sense to expose the error produced by it.
In contrast, a function which makes several calls to a database probably should
not return an error which unwraps to the result of one of those calls. If the
database used by the function is an implementation detail, then exposing these
errors is a violation of abstraction. For example, if the `LookupUser` function
of your package `pkg` uses Go's `database/sql` package, then it may encounter a
`sql.ErrNoRows` error. If you return that error with
`fmt.Errorf("accessing`DB:`%v",`err)`
then a caller cannot look inside to find the `sql.ErrNoRows`. But if
the function instead returns `fmt.Errorf("accessing`DB:`%w",`err)`, then a
caller could reasonably write
err := pkg.LookupUser(...)
if errors.Is(err, sql.ErrNoRows) …
At that point, the function must always return `sql.ErrNoRows` if you don't want
to break your clients, even if you switch to a different database package. In
other words, wrapping an error makes that error part of your API. If you don't
want to commit to supporting that error as part of your API in the future, you
shouldn't wrap the error.
Its important to remember that whether you wrap or not, the error text will be
the same. A _person_ trying to understand the error will have the same information
either way; the choice to wrap is about whether to give _programs_ additional
information so they can make more informed decisions, or to withhold that
information to preserve an abstraction layer.
* Customizing error tests with Is and As methods
The `errors.Is` function examines each error in a chain for a match with a
target value. By default, an error matches the target if the two are equal. In
addition, an error in the chain may declare that it matches a target by
implementing an `Is` _method_.
As an example, consider this error inspired by the
[[https://commandcenter.blogspot.com/2017/12/error-handling-in-upspin.html][Upspin error package]]
which compares an error against a template, considering only fields which are
non-zero in the template:
type Error struct {
Path string
User string
}
func (e *Error) Is(target error) bool {
t, ok := target.(*Error)
if !ok {
return false
}
return (e.Path == t.Path || t.Path == "") &&
(e.User == t.User || t.User == "")
}
if errors.Is(err, &Error{User: "someuser"}) {
// err's User field is "someuser".
}
The `errors.As` function similarly consults an `As` method when present.
* Errors and package APIs
A package which returns errors (and most do) should describe what properties of
those errors programmers may rely on. A well-designed package will also avoid
returning errors with properties that should not be relied upon.
The simplest specification is to say that operations either succeed or fail,
returning a nil or non-nil error value respectively. In many cases, no further
information is needed.
If we wish a function to return an identifiable error condition, such as "item
not found," we might return an error wrapping a sentinel.
var ErrNotFound = errors.New("not found")
// FetchItem returns the named item.
//
// If no item with the name exists, FetchItem returns an error
// wrapping ErrNotFound.
func FetchItem(name string) (*Item, error) {
if itemNotFound(name) {
return nil, fmt.Errorf("%q: %w", name, ErrNotFound)
}
// ...
}
There are other existing patterns for providing errors which can be semantically
examined by the caller, such as directly returning a sentinel value, a specific
type, or a value which can be examined with a predicate function.
In all cases, care should be taken not to expose internal details to the user.
As we touched on in "Whether to Wrap" above, when you return
an error from another package you should convert the error to a form that does
not expose the underlying error, unless you are willing to commit to returning
that specific error in the future.
f, err := os.Open(filename)
if err != nil {
// The *os.PathError returned by os.Open is an internal detail.
// To avoid exposing it to the caller, repackage it as a new
// error with the same text. We use the %v formatting verb, since
// %w would permit the caller to unwrap the original *os.PathError.
return fmt.Errorf("%v", err)
}
If a function is defined as returning an error wrapping some sentinel or type,
do not return the underlying error directly.
var ErrPermission = errors.New("permission denied")
// DoSomething returns an error wrapping ErrPermission if the user
// does not have permission to do something.
func DoSomething() error {
if !userHasPermission() {
// If we return ErrPermission directly, callers might come
// to depend on the exact error value, writing code like this:
//
// if err := pkg.DoSomething(); err == pkg.ErrPermission { … }
//
// This will cause problems if we want to add additional
// context to the error in the future. To avoid this, we
// return an error wrapping the sentinel so that users must
// always unwrap it:
//
// if err := pkg.DoSomething(); errors.Is(err, pkg.ErrPermission) { ... }
return fmt.Errorf("%w", ErrPermission)
}
// ...
}
* Conclusion
Although the changes weve discussed amount to just three functions and a
formatting verb, we hope they will go a long way toward improving how errors are
handled in Go programs. We expect that wrapping to provide additional context
will become commonplace, helping programs to make better decisions and helping
programmers to find bugs more quickly.
As Russ Cox said in his [[https://blog.golang.org/experiment][GopherCon 2019 keynote]],
on the path to Go 2 we experiment, simplify and ship. Now that weve
shipped these changes, we look forward to the experiments that will follow.