| Working with Errors in Go 1.13 |
| 17 Oct 2019 |
| Tags: errors, technical |
| |
| Damien Neil and Jonathan Amsterdam |
| |
| * Introduction |
| |
| Go’s treatment of [[https://blog.golang.org/errors-are-values][errors as values]] |
| has served us well over the last decade. Although the standard library’s support |
| for errors has been minimal—just the `errors.New` and `fmt.Errorf` functions, |
| which produce errors that contain only a message—the 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. |
| |
| It’s 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 we’ve 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 we’ve |
| shipped these changes, we look forward to the experiments that will follow. |