Gracefully handling errors, not just checking for errors

Source: Internet
Author: User
Tags readfile unpack

This is a creation in Article, where the information may have evolved or changed. This article was extracted to my speech at [Gocon Spring Conference] (HTTPS://GOCON.CONNPASS.COM/EVENT/27521/) in Tokyo, Japan. [] (https://raw.githubusercontent.com/studygolang/gctt-images/master/error-handle/ba5a9ada.png) 


The error is just some values
I spent a lot of time thinking about how to handle errors in Go is the best. I really hope there is a simple and straightforward way to handle errors. Some rules we can just use for Go programmers to remember are like teaching math or alphabet.

However, I came to the conclusion that there is more than one way to handle errors. I think Go's approach to error can be divided into 3 main strategies.

Mark error strategy
The first error handling strategy, I call it tag errors

if err == ErrSomething {…}
This name comes from the fact that in actual programming, a specified value is used to indicate that the program can no longer execute. So in Go we use a specified value to indicate an error.

For example: io.EOF in the system package or lower level constant errors in the syscall package such as syscall.ENOENT.

There are even flags indicating that no errors occurred such as: go / build.NoGoError and path / filepath.SkipDir in path / filepath.Walk.

Using tag values is one of the least flexible error handling strategies. The caller must use the equality operator to compare the return value with a predefined value. When you want to provide more relevant information, returning different error values can break the equation checking operation.

Even if you provide more information through fmt.Errorf, it will interfere with the caller's equality test. The caller must see whether the output of the Error method matches a specified string.

Never check the output of error.Error
By the way, I believe you never need to check the return value of the error.Error method. The Error method in the error interface is provided to the user to view the information, not to judge the code.

This information should appear in the log file or on the display. You do not need to check the information to change the program behavior.

I know sometimes this is difficult, as some people have mentioned on twitter, this advice does not apply when writing tests. However, in my opinion, as a coding style, you should avoid comparing character-type error messages.

Tag errors as part of a public API
If your public function or method returns some specified error values, then those values must be public and of course need to be described in the documentation. These are added to your API.

If your API defines an interface that returns a specified error, all implementations of that interface must return only this error, and even if it can provide more additional information, it should not return information other than the specified error.

We can see this in io.Reader. io.Copy requires the reader implementation to return io.EOF to notify the caller that there is no more data, but this is not an error.

Marking errors create dependencies between two packages
The biggest problem is that markup errors create source-level dependencies between the two packages. For example: check if an error is io.EOF. Your code must include the io package.

This example doesn't look so bad, because it is very common operation. But imagine that many packages in the project export error values, and other packages must import corresponding packages to check for error conditions, which violates the design principle of low coupling.

I have participated in a large project that uses this error handling mode. I can tell you that the loop introduction problem caused by bad design is close at hand.

Conclusion: Avoiding Tag Error Strategies
So, my recommendation is to avoid using tag error handling strategies in your code. There are some cases in the standard library that use this processing method, but this is not a processing mode that you should follow.

If someone asks you to expose an error value from your bag, you should politely reject him and provide an alternative, which is the method mentioned below.

Error type
Error types are the second Go error handling mode I want to discuss.

if err, ok: = err. (SomeType); ok {…}
The error type is the type you created that implements the error interface. In the following example, the MyError type records the file, line number, and related error information.

type MyError struct {
    Msg string
    File string
    Line int
}

func (e * MyError) Error () string {
    return fmt.Sprintf ("% s:% d:% s", e.File, e.Line, e.Msg)
}

return & MyError {"Something happened", "server.go", 42}
Because MyError is a type, callers can use type assertion to get relevant information from error.

err: = something ()
switch err: = err. (type) {
case nil:
    // call succeeded, nothing to do
case * MyError:
    fmt.Println ("error occurred on line:", err.Line)
default:
    // unknown error
}
The biggest improvement of error types over flagging errors is to provide more relevant information by encapsulating the underlying errors.

A great example is that os.PathError provides information about which file to use and which operation to perform, in addition to the underlying error.

// PathError records an error and the operation
// and file path that caused it.
type PathError struct {
    Op string
    Path string
    Err error // the cause
}

func (e * PathError) Error () string
Problems with error types
The caller can use type assertion or type switch, and the error type must be public.

If your code implements an interface that specifies an error type, all implementers of this interface depend on the package that defines the error type.

Excessive exposure to the wrong types of packages creates a strong coupling between the caller and the package, leading to the vulnerability of the API.

Conclusion: Avoiding wrong types
Although error types can capture more environmental information when errors occur, which is better than marking errors, there are many problems with error types that are similar to marking errors.

So, my advice here is to avoid using the wrong types, at least avoid making them part of your API interface.

Encapsulation error
Now we reach the third error handling classification. In my opinion this is the most flexible processing strategy, with the least coupling between the caller and your code.

I call this method of handling errors (Opaque errors), because when you find an error, you can't know the internal error condition. As the caller, you only know the success or failure of the result of the call.

The package error handling method only returns errors without guessing his content. If you use this approach, error handling can become very valuable for debugging.

import “github.com/quux/bar”

func fn () error {
    x, err: = bar.Foo ()
    if err! = nil {
        return err
    }
    // use x
}
For example: The calling convention of Foo does not specify what relevant information will be returned when an error occurs, so that the developer of the Foo function can freely provide relevant error information without affecting the agreement with the caller.

Assertion behavior, not type
In a few cases, this binary error handling scheme is not enough.

For example: when interacting with out-of-process, such as network activities, the caller needs to evaluate the error condition to decide whether to retry the operation.

In this case we assert that the incorrect implementation of the specified behavior is better than asserting the specified type or value. Take a look at the following example:

type temporary interface {
    Temporary () bool
}

// IsTemporary returns true if err is temporary.
func IsTemporary (err error) bool {
    te, ok: = err. (temporary)
    return ok && te.Temporary ()
}
We can pass any error to IsTemporary to determine if the error needs to be retried.

If the error does not implement the temporary interface; then there is no Temporary method, and the error is not temporary.

If Temporary is implemented by mistake, the caller can consider retrying the operation if Temporary returns true.

The key point here is that the implementation logic does not need to import the package that defines the error or understand any underlying types about the error, we just need to focus on its behavior.

Handle errors gracefully, not just check them
This leads me to the second Go motto I want to talk about: Handle errors gracefully, not just check for errors. Can you find the error in the code below?

func AuthenticateRequest (r * Request) error {
    err: = authenticate (r.User)
    if err! = nil {
        return err
    }
    return nil
}
An obvious suggestion is that the above code can be simplified to

return authenticate (r.User)
But this is just a simple question, and anyone should see it during code review. The more fundamental problem is that this code does not see where the original error occurred.

If authenticate returns an error, then AuthenticateRequest will return an error to the caller, and the caller will return the same. Print the error message to the screen or log file in the main function block of the top layer of the program, but all the information is No such file or directory



There are no errors, file numbers, line numbers, etc., nor call stack information. The writer of the code must find in a bunch of functions which call path returns a file not found error.

The Go Programming Language by Donovan and Kernighan suggests that you use fmt.Errorf to add relevant information to the error path

func AuthenticateRequest (r * Request) error {
    err: = authenticate (r.User)
    if err! = nil {
        return fmt.Errorf ("authenticate failed:% v", err)
    }
    return nil
}
As we mentioned earlier, this mode is not compatible with flagging errors or type assertions, because the error value is converted to a string, combined with other strings, and then converted to error using fmt.Errorf to break the peer relationship and destroy Information about the original error.

Annotation error
Here I suggest a way to add relevant information to the error, using a simple package. The code is at github.com/pkg/errors. This package has two main functions:

// Wrap annotates cause with a message.
func Wrap (cause error, message string) error
The first function is a wrapper function Wrap that takes an error and a message and generates a new error return.

// Cause unwraps an annotated error.
func Cause (err error) error
The second function is Cause. Enter a encapsulated error and get the original error message after unpacking.

Using these two functions, we can now add relevant information to any error and unpack it when we need to see the underlying error type. The following example is a function that reads the contents of a file into memory.

func ReadFile (path string) ([] byte, error) {
    f, err: = os.Open (path)
    if err! = nil {
        return nil, errors.Wrap (err, "open failed")
    }
    defer f.Close ()

    buf, err: = ioutil.ReadAll (f)if err! = nil {
        return nil, errors.Wrap (err, "read failed")
    }
    return buf, nil
}
We use this function to write a function that reads the configuration file and then calls it in main.

func ReadConfig () ([] byte, error) {
    home: = os.Getenv ("HOME")
    config, err: = ReadFile (filepath.Join (home, ".settings.xml"))
    return config, errors.Wrap (err, "could not read config")
}

func main () {
    _, err: = ReadConfig ()
    if err! = nil {
        fmt.Println (err)
        os.Exit (1)
    }
}
If an error occurs in ReadConfig, we can get a K & D-style error with relevant information due to the use of errors.Wrap

could not read config: open failed: open /Users/dfc/.settings.xml: no such file or directory
Because errors.Wrap generates call stack information when errors occur, we can view additional call stack debugging information. Here is another example, but this time we replaced fmt.Println with errors.Print

func main () {
    _, err: = ReadConfig ()
    if err! = nil {
        errors.Print (err)
        os.Exit (1)
    }
}
We will get the following information:

readfile.go: 27: could not read config
readfile.go: 14: open failed
open /Users/dfc/.settings.xml: no such file or directory
The first line comes from ReadConfig, the second line comes from ReadFile of os.Open, and the rest comes from the os package, which does not carry location information.

Now that we have introduced the concept of packaging error generation stacks, we need to talk about how to unpack. Here is what the errors.Cause function does.

// IsTemporary returns true if err is temporary.
func IsTemporary (err error) bool {
    te, ok: = errors.Cause (err). (temporary)
    return ok && te.Temporary ()
}
In operation, when you need to check whether an error matches a specified value or type, you need to first use errors.Cause to get the original error information

Handle errors only once
The last thing I want to say is that you only need to handle errors once. Handling errors means checking for erroneous values and then making a decision.

func Write (w io.Writer, buf [] byte) {
    w.Write (buf)
}
If you don't need to make a decision, you can ignore this error. In the above example you can see that we ignored the error returned by w.Write.

But making multiple decisions when returning an error is also problematic.

func Write (w io.Writer, buf [] byte) error {
    _, err: = w.Write (buf)
    if err! = nil {
        // annotated error goes to log file
        log.Println ("unable to write:", err)

        // unannotated error returned to caller
        return err
    }
    return nil
}
In this example, if a Write error occurs, a line of information is written to the log, which records the file and line number of the error, and returns the error to the caller. The same caller may also write to the log and then return until the program Topmost.

There will be a bunch of duplicate information in the log file, but the original error obtained at the top of the program has no relevant information.

func Write (w io.Write, buf [] byte) error {
    _, err: = w.Write (buf)
    return errors.Wrap (err, "write failed")
}
Using the errors package allows you to add relevant information to the error, and the content can be recognized by people and machines.

in conclusion
Finally, errors are part of the public APIs in the packages you provide, and you should treat them as carefully as other public APIs.

For maximum flexibility, I suggest that you try to treat all errors as encapsulated errors. In those cases where you can't, assert the wrong behavior, not the type or value.

Use as few errors as possible in your program and use errors.Wrap as early as possible when errors occur.

related articles
check for errors
Constant error
Call stack and error packets
Errors and exceptions returned

Related Article

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.