Five mechanisms of Go error handling [Go language Bible notes]

Posted by dxdolar on Fri, 10 Sep 2021 06:22:24 +0200

Error handling strategy

  1. Error propagation
  2. retry
  3. Output the error and end the program
  4. Output error message
  5. Ignore directly

When a function call returns an error, the caller should choose the appropriate way to handle the error. There are many processing methods according to different situations. Let's take a look at the five commonly used methods.

The first and most common way is to propagate errors. This means that the failure of a subprogram in the function will become the failure of the function. Next, we take the findLinks function in section 5.3 as an example. If findLinks fails to call http.Get, findLinks will directly return this HTTP error to the caller:

resp, err := http.Get(url)
if err != nil {
    return nil, err
}

When the call to html.Parse fails, findLinks will not directly return the error of html.Parse because two important information are missing: 1. The parser when the error occurs (html parser); 2. The url where the error occurred. Therefore, findLinks constructs a new error message, including both these two items and the underlying parsing error information.

doc, err := html.Parse(resp.Body)
resp.Body.Close()
if err != nil {
    return nil, fmt.Errorf("parsing %s as HTML: &v", url, err)
}

The fmt.errorffunction formats the error message using fmt.Sprintf and returns it. We use this function to add additional prefix context information to the original error message. When the error is finally handled by the main function, the error message should provide a clear causal chain from cause to consequence, just as NASA did during accident investigation:

genesis: crashed: no parachute: G-switch failed: bad relay orientation

Since error messages are often combined in a chain, uppercase and newline characters should be avoided in error messages. The final error message may be very long, and we can process the error message through a tool like grep.

When writing an error message, we should ensure that the error message describes the details of the problem in detail. In particular, pay attention to the consistency of error information expression, that is, the errors returned by the same function or the same group of functions in the same package are similar in composition and processing.

Taking the os package as an example, the os package ensures that the description of each error returned by file operations (such as os.Open, Read, Write, Close) includes not only the cause of the error (such as no permission, the file directory does not exist) but also the file name, so that the caller does not need to add these information when constructing new error information.

Generally speaking, the called function f(x) will put the call information and parameter information in the error information as the context of the error and return it to the caller. The caller needs to add some information not contained in the error information, such as adding url to the error returned by html.Parse.

Let's look at the second strategy for handling errors. If the error is accidental or caused by unpredictable problems. A wise choice is to retry the failed operation. When retrying, we need to limit the time interval or number of retries to prevent unlimited retries.

// WaitForServer attempts to contact the server of a URL.
// It tries for one minute using exponential back-off.
// It reports an error if all attempts fail.

func WaitForServer(url string) error {
    const timeout = 1 * time.Minute
    deadline := time.Now().Add(timeout)
    for tries := 0; time.Now().Before(deadline); tries++ {
        _, err := http.Head(url)
        if err == nil {
            return nil
        }
        log.Printf("server not responding (%s): retrying...", err)
        time.Sleep(time.Second << uint(tries))  // exponetial back-off
    }
    return fmt.Errorf("server %s failed to respond after %s", url, timeout)
}

If the program cannot continue to run after the error occurs, we can adopt a third strategy: output the error message and end the program. Note that this strategy should only be executed in main. For library functions, errors should only be propagated upward. Unless the error means that the program contains inconsistencies, that is, a bug is encountered, the program can be ended in the library function.

// (In function main.)
if err := WaitForServer(url): err != nil {
    fmt.Fprintf(os.Stderr, "Site is down: %v\n", err)
    os.Exit(1)  // forced return
}

Calling log.Fatalf can achieve the same effect as above with more concise code. All functions in log will output time information before error information by default.

if err := WaitForServer(url); err != nil {
    log.Fatalf("Site is down: %v\n", err)
}

Time running servers often use the default time format, while interactive tools rarely use the format containing so much information.

2006/01/02 15:04:05 Site is down: no such domain:
bad.gopl.io

We can set the prefix information of log to mask the time information. Generally speaking, the prefix information will be set as the command name.

log.SetPrefix("wait: ")
log.SetFlags(0)

The fourth strategy: sometimes, we only need to output error information, without interrupting the operation of the program. We can provide functions through the log package

if err := Ping(); err != nil {
    log.Printf("ping failed: %v; networking disabled", err)
}

Or the standard error stream outputs an error message.

if err := Ping(); err != nil {
    fmt.Fprintf(os.Stderr, "ping failed: %v; networking disabled\n", err)
} 

All functions in the log package add line breaks to strings without line breaks.

The fifth and final strategy: we can ignore mistakes directly.

dir, err := ioutil.TempDir("", "scratch")
if err != nil {
    return fmt.Errorf("failed to create temp dir: %v", err)
}

// ...use temp dit...
os.RemoveAll(dir)  // gnore errors; $TMPDIR is cleaned periodically

Although os.RemoveAll will fail, the above example does not do error handling. This is because the operating system will periodically clean up the temporary directory. Because of this, although the program does not handle errors, the logic of the program will not be affected. We should form the habit of considering error handling after each function call. When you decide to ignore an error, you should clearly write down your intention.

In Go, error handling has a unique coding style. After checking whether a sub function fails, we usually put the failed logic code before the successful code. If an error will cause the function to return, the logic code at the time of success should not be placed in the else statement block, but directly in the function body. The code structure of most functions in Go is almost the same. First, a series of initial checks to prevent errors, followed by the actual logic of the function.

Topics: Go