Over the past decade, we have successfully exploited the fact that Go handles errors as values . Although the standard library had minimal support for errors: only the
errors.New
and
fmt.Errorf
functions that generate an error containing only a message - the built-in interface allows Go-programmers to add any information. All you need is a type that implements the
Error
method:
type QueryError struct { Query string Err error } func (e *QueryError) Error() string { return e.Query + ": " + e.Err.Error() }
These types of errors are found in all languages and store a wide variety of information, from timestamps to file names and server addresses. Low-level errors that provide additional context are often mentioned.
A pattern where one error contains another is so common in Go that after a heated discussion in Go 1.13 its explicit support was added. In this article, we will look at additions to the standard library that provide the mentioned support: three new functions in the errors package and a new formatting command for
fmt.Errorf
.
Before discussing the changes in detail, let's talk about how errors were investigated and constructed in previous versions of the language.
Errors before Go 1.13
Error research
Errors in Go are meanings. Programs make decisions based on these values in different ways. Most often, the error is compared to nil to see if the operation failed.
if err != nil { // something went wrong }
Sometimes we compare the error to find out the control value and to understand if a specific error has occurred.
var ErrNotFound = errors.New("not found") if err == ErrNotFound { // something wasn't found }
The error value can be of any type that satisfies the error interface defined in the language. A program can use a type statement or a type switch to view the error value of 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
Often a function passes an error up the call stack, adding information to it, for example, a short description of what happened when the error occurred. This is easy to do, just construct a new error that includes the text from the previous error:
if err != nil { return fmt.Errorf("decompress %v: %v", name, err) }
When creating a new error using
fmt.Errorf
we discard everything except the text from the original error. As we saw in the
QueryError
example, sometimes you need to define a new type of error that contains the original error in order to save it for analysis using code:
type QueryError struct { Query string Err error }
Programs can look inside the
*QueryError
and make a decision based on the original error. This is sometimes called the “unwrapping” of an error.
if e, ok := err.(*QueryError); ok && e.Err == ErrPermission { // query failed because of a permission problem }
The
os.PathError
type from the standard library is another example of how one error contains another.
Errors in Go 1.13
Unwrap Method
In Go 1.13, the
errors
and
fmt
standard library packages simplified
fmt
errors that contain other errors. The most important is the convention, not the change: an error containing another error can implement the
Unwrap
method, which returns the original error. If
e1.Unwrap()
returns
e2
, then we say that
e1
packs
e2
and you can unpack
e1
to get
e2
.
According to this convention, you can give the
QueryError
type described above to the
QueryError
method, which returns the error it contains:
func (e *QueryError) Unwrap() error { return e.Err }
The result of unpacking the error may also contain the
Unwrap
method. The sequence of errors obtained by repeated unpacking, we call the chain of errors .
Investigating Errors Using Is and As
In Go 1.13, the
errors
package contains two new functions for investigating errors:
Is
and
As
.
The
errors.Is
function compares an error with a value.
// Similar to: // if err == ErrNotFound { … } if errors.Is(err, ErrNotFound) { // something wasn't found }
The
As
function checks to see if the error is of a particular 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 with a control error, and the
errors.As
function behaves like a type statement. However, when working with packed errors, these functions evaluate all errors in the chain. Let's look at the above
QueryError
example to examine the original error:
if e, ok := err.(*QueryError); ok && e.Err == ErrPermission { // query failed because of a permission problem }
Using the
errors.Is
function,
errors.Is
can write this:
if errors.Is(err, ErrPermission) { // err, or some error that it wraps, is a permission problem }
The
errors
package also contains a new
Unwrap
function that returns the result of calling the
Unwrap
method of the error, or returns nil if the error does not have the
Unwrap
method. It is usually better to use
errors.Is
or
errors.As
, since they allow you to examine the entire chain with a single call.
Error packaging with% w
As I mentioned, it is normal practice to use the
fmt.Errorf
function to add additional information to the error.
if err != nil { return fmt.Errorf("decompress %v: %v", name, err) }
In Go 1.13, the
fmt.Errorf
function supports the new
%w
command. If it is, then the error returned by
fmt.Errorf
will contain the
Unwrap
method that returns the argument
%w
, which should be an error. In all other cases,
%w
identical to
%v
.
if err != nil { // Return an error which unwraps to err. return fmt.Errorf("decompress %v: %w", name, err) }
Packing the error with
%w
makes it available for
errors.Is
and
errors.As
:
err := fmt.Errorf("access denied: %w", ErrPermission) ... if errors.Is(err, ErrPermission) ...
When to pack?
When you add an additional context to the error using
fmt.Errorf
or a custom type implementation, you need to decide whether the new error will contain the original. There is no single answer to this, it all depends on the context in which the new error is created. Pack to show her caller. Do not package the error if this leads to disclosure of implementation details.
For example, imagine a
Parse
function that reads a complex data structure from
io.Reader
. If an error occurs, we will want to find out the number of the row and column where it occurred. If an error occurred while reading from
io.Reader
, we will need to pack it to find out the reason. Since the caller was provided with the
io.Reader
function, it makes sense to show the error that it generated.
Another case: a function that makes several database calls probably should not return an error in which the result of one of these calls is packed. If the database used by this function is part of the implementation, then disclosing these errors will violate the abstraction. For example, if the
LookupUser
function from the
pkg
package uses the Go
database/sql
package, then it may encounter the
sql.ErrNoRows
error. If you return an error using
fmt.Errorf("accessing DB: %v", err)
, then the caller cannot look inside and find
sql.ErrNoRows
. But if the function returns
fmt.Errorf("accessing DB: %w", err)
, then the caller could write:
err := pkg.LookupUser(...) if errors.Is(err, sql.ErrNoRows) …
In this case, the function should always return
sql.ErrNoRows
if you do not want to break clients, even when switching to a package with a different database. In other words, packaging makes an error part of your API. If you do not want to commit support for this error in the future as part of the API, do not package it.
It is important to remember that regardless of whether you pack it or not, the error will remain unchanged. A person who will understand it will have the same information. Making decisions about packaging depends on whether additional information is needed for programs so that they can make more informed decisions; or if you want to hide this information in order to maintain the level of abstraction.
Setting Up Error Testing Using Is and As Methods
The
errors.Is
function checks every error in the chain against the target value. By default, an error matches this value if they are equivalent. In addition, an error in the chain can declare its compliance with the target value using the implementation of the
Is
method .
Consider the error caused by the Upspin package , which compares the error with the template and evaluates only nonzero fields:
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 also advises the
As
method, if any.
Errors and Package APIs
A package that returns errors (and most packages do this) should describe the properties of these errors that the programmer can rely on. A well-designed package will also avoid returning errors with properties that cannot be relied upon.
The simplest thing is to say whether the operation was successful, returning, respectively, the value nil or non-nil. In many cases, other information is not required.
If you need the function to return an identifiable error state, for example, “element not found”, then you can return an error in which the signal value is packed.
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 patterns for providing errors that the caller can semantically examine. For example, directly return a control value, a specific type, or a value that can be analyzed using a predicative function.
In any case, do not disclose the internal details to the user. As mentioned in the chapter “When is it worth packaging?”, If you return an error from another package, then convert it so as not to reveal the original error, unless you intend to commit yourself to return this 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 returns an error with a packed signal value or type, then do not directly return the original error.
var ErrPermission = errors.New("permission denied") // DoSomething returns an error wrapping ErrPermission if the user // does not have permission to do something. func DoSomething() { 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 we discussed only three functions and a formatting command, we hope that they will help greatly improve error handling in Go programs. We hope that packaging for the sake of providing additional context will become a normal practice, helping programmers make better decisions and find bugs faster.
As Russ Cox said in his speech at GopherCon 2019 , on the way to Go 2 we experiment, simplify and ship. And now, having shipped these changes, we are embarking on new experiments.