Dealing with bugs in Go 1.13







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.



All Articles