Source: www.cyningsun.com/09-09-2019/…

This digest was recently a speaker at the GoCon Spring conference in Tokyo, Japan.

Errors are just values

I spent a lot of time thinking about the best way to handle errors in Go programs. I wish there was a single error-handling method that could be taught to all Go programmers by rote, like math or the English alphabet.

However, I have come to the conclusion that there is no single way to handle errors. Instead, I believe that Go’s error handling can be divided into three core strategies.

Sentinel errors

The first type of error handling is what I call _Sentinel errors_.

ifErr == ErrSomething {... }Copy the code

The name comes from the computer programming practice of using specific values to indicate that no further processing is possible. Therefore, for Go, we use a specific value to indicate an error.

Examples include the value of the io.eof class, or lower-level errors such as syscall.enoent in the syscall package.

There are even Sentinel errors indicating _ no _ errors, such as go/ build.nogoError, and path/ filepath.skipdir for path/filepath.

Using Sentinel values is the least flexible error-handling strategy, because the caller must use the equals operator to compare the result to a pre-declared value. The problem occurs when you want to provide more context, because returning a different error breaks the equality check.

Even well-intentioned use of FMT.Errorf to add some context to the error will cause the caller’s equality test to fail. In turn, the caller is forced to look at the output of the error method of error to see if it matches a particular string.

Never inspect the output of error.Error

Also, I think you should never check the output of the error-error method. The ERROR method on the Error interface is for humans, not code.

The contents of the string belong to a log file or are displayed on the screen. You should not try to change the behavior of the program by examining it.

I know this is sometimes impossible, but as someone pointed out on Twitter, this advice doesn’t apply to writing tests. More importantly, in my opinion, comparing the wrong string form is a code smell that you should try to avoid.

Sentinel errors become part of your public API

If your public function or method returns an error with a particular value, then the value must be public and, of course, documented. This will increase the area of the API.

If your API defines an interface that returns a particular error, all implementations of that interface will be limited to returning only that error, even though they may provide more descriptive errors.

See this through IO.Reader. A function like IO.Copy requires a Reader implementation to return IO.EOF _ precisely _ to signal to the caller that there is no more data, but this is not an error.

Sentinel errors create a dependency between two packages

By far the biggest problem with Sentinel Error values is that they create source code dependencies between the two packages. For example, to check if the error is equal to IO.EOF, your code must import the IO package.

This concrete example doesn’t sound so bad because it’s so common, but imagine the coupling that exists when many packages in a project export error values and other packages in the project must import to check for specific error conditions.

Having worked on a large project that played with this pattern, I can tell you that the ghost of bad design in the form of import loops is never far from our minds.

Conclusion: avoid sentinel errors

So, my advice is to avoid using Sentinel Error values in your code. In some cases, they will be used in the standard library, but you should not emulate this pattern.

If someone asks you to export error values from a package, you should politely decline and instead suggest an alternative approach, such as the one I’ll discuss below.

Error types

Error Types is the second form of Go Error handling THAT I want to discuss.

iferr, ok := err.(SomeType); Ok {... }Copy the code

The error type is the type of interface that you created to implement the error. In this example, the MyError type tracks the file and the line, along with a message explaining what happened.

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}
Copy the code

Since MyError Error is a type, the caller can use type assertions to extract additional context from the error.

err := something()
switch err := err.(type) {
case nil:
// call succeeded, nothing to do
casePrintln(" Error occurred on line: ", err.line) *MyError: fmt.Println(" Error occurred on line: ", err.line)default:
// unknown error
}
Copy the code

A major improvement of Error types over Error values is their ability to wrap the underlying error to provide more context.

A good example is the os.patherror type, which comments the underlying error by the operation it is trying to perform and the file it is trying to use.

// 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(a) string
Copy the code
Problems with error types

Callers can use type assertions or type switch, error types must be public.

If your code implements an interface whose contract requires a specific error type, then all implementers of that interface need to rely on the package that defines the error type.

A deep understanding of package types creates a strong coupling with the caller, resulting in a fragile API.

Conclusion: avoid error types

Although Error types are better than Sentinel Error Values because they can capture more context about the error, error types also have many problems with error values.

So my advice is to avoid error types, or at least to avoid making them part of the public API.

Opaque errors

Now let’s look at the third type of error handling. In my opinion, this is the most flexible error-handling strategy because it requires minimal coupling between code and caller.

I call this opaque error handling, because while you know an error has occurred, you can’t see inside the error. As the caller, all your knowledge of the results of the operation is valid or not.

This is opaque error handling – just returning the error without assuming its contents. If you take this approach, error handling becomes very useful as a debugging aid.

import"Github.com/quux/bar"func fn(a) error {
	x, err := bar.Foo()
	iferr ! =nil {
		return err
	}
	// use x
}
Copy the code

For example, Foo’s contract does not guarantee what it will return in the wrong context. By passing the error with additional context, the author of Foo is now free to comment the error without violating the contract with the caller.

Assert errors for behaviour, not type

In rare cases, using dichotomies (whether there is an error) for error handling is not sufficient.

For example, interactions with services outside the process, such as network activity, require the caller to look at the nature of the error to determine whether the retry operation is justified.

In this case, we can assert that the error implements a specific behavior, rather than that the error is a specific type or value. Consider this 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()
}
Copy the code

You can pass any errors to istemtempo to determine if the error can be retried.

If the error does not implement the TEMPORARY interface; That is, it has no Temporary method, so the error is not Temporary.

If the error does implement Temporary, then the caller can retry the operation if true returns true.

The key here is that this logic can be implemented without importing the error-defined package, or knowing anything directly about the underlying type of ERR – we’re just interested in its behavior.

Don’t just check errors, handle them gracefully

Which brings me to the second Go proverb that I want to talk about; Don’t just check for errors, handle them gracefully. Can you ask some questions with the following code?

func AuthenticateRequest(r *Request) error {
	err := authenticate(r.User)
	iferr ! =nil {
		return err
	}
	return nil
}
Copy the code

An obvious suggestion is to replace the five lines of the function with:

return authenticate(r.User)
Copy the code

But this is a simple problem that everyone should find in a code review. The more fundamental problem with this code is that it is impossible to tell where the original error came from.

If Authenticate returns an error, then AuthenticateRequest returns the error to the caller, who may also do so, and so on. At the top of the program, the body of the program prints the error to a screen or log file, all of which will be: No such file or directory.

Error file and line information is not generated. There is no stack trace of the call stack that caused the error. The authors of this code will be forced to have a long session, splitting their code into two halves, to discover which code path triggered the file not found error.

Donovan and Kernighan’s _The Go Programming Language_ recommends that you use FMT.errorf to add context to the error path

func AuthenticateRequest(r *Request) error {
	err := authenticate(r.User)
	iferr ! =nil {
		return **fmt.Errorf("authenticate failed: %v", err)**
	}
	return nil
}
Copy the code

But as we saw earlier, this pattern is not compatible with using Sentinel Error values or type assertions because converting an error value to a string, merging it with another string, and then converting it back to an error using FMt.errorf breaks equality, It also completely destroys the context in the original error.

Annotating errors

I’d like to suggest a way to add context to errors, and to do so, I’ll introduce a simple package. The code is available at github.com/pkg/errors. Error packages have two main functions:

// Wrap annotates cause with a message.
func Wrap(cause error, message string) error
Copy the code

The first function is Wrap, which receives an error and a message and generates a new error.

// Cause unwraps an annotated error.
func Cause(err error) error
Copy the code

The second function is Cause, which receives the error that may have been wrapped and unpacks it to restore the original error.

Using these two functions, we can now comment any errors and recover the underlying errors if we need to check. Consider the example of a function that reads the contents of a file into memory.

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

	buf, err := ioutil.ReadAll(f)
	iferr ! =nil {
		return nil, **errors.Wrap(err, "read failed") * *}return buf, nil
}
Copy the code

We’ll use this function to write a function to read the configuration file and then call it from main.

func ReadConfig(a) ([]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(a) {
	_, err := ReadConfig()
	iferr ! =nil {
		fmt.Println(err)
		os.Exit(1)}}Copy the code

If the ReadConfig code path fails because we used errors.Wrap, we get a nice comment error in the K&D style.

could not read config: open failed: open /Users/dfc/.settings.xml: no such file or directory
Copy the code

Because errors.wrap produces a stack error, we can examine the stack for additional debugging information. This is the same example again, but this time we replace errors.Print with fmt.println

func main(a) {
	_, err := ReadConfig()
	iferr ! =nil {
		errors.Print(err)
		os.Exit(1)}}Copy the code

We 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
Copy the code

The first line is from ReadConfig, the second line is from the os.open section of ReadFile, and the rest is from the OS package itself, which carries no location information.

Now that we’ve introduced the concept of wrapping error-generating stacks, we need to talk about doing the reverse and unwrapping them. This is the field of the errors.Cause function.

// IsTemporary returns true if err is temporary.
func IsTemporary(err error) bool {
	te, ok := **errors.Cause(err)**.(temporary)
	return ok && te.Temporary()
}
Copy the code

In an operation, whenever you need to check whether an error matches a particular value or type, you should first restore the original error using the errors.Cause function.

Only handle errors once

Finally, I’d like to mention that you should only handle one error. Handling errors means checking for error values and making a decision.

func Write(w io.Writer, buf []byte) {
	w.Write(buf)
}
Copy the code

If no decision is made, the error is ignored. As we can see here, the W.Rite error was discarded.

However, there are also problems with making multiple decisions based on a single error.

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

		// unannotated error returned to caller
		return err
	}
	return nil
}
Copy the code

In this example if an error occurs during Write, a line will be written to a log file, noting the file and line that the error occurred, and the error is also returned to the caller, who possibly will log it, and return it, all the way back up to the top of the program.

So you get a stack of duplicate lines in your log file, but at the top of the program you get the original error without any context. Java anyone?

In this example, if an error occurs during Write, a line is written to a log file, note the file and line where the error occurred, and the error is also returned to the caller, who may log it and return it, all the way back to the top of the program.

So, you get a stack of repeating lines in the log file, but at the top of the program, you get any context without the original error. Does anybody use Java?

func Write(w io.Write, buf []byte) error {
	_, err := w.Write(buf)
	return **errors.Wrap(err, "write failed") * *}Copy the code

With the Errors package, you can add context to error values in a way that both people and machines can check.

Conclusion

In short, errors are part of the package public API and should be treated with the same care as any other part of the public API.

For maximum flexibility, I recommend that you try to treat all errors as opaque. In cases where this cannot be done, assert a behavior error, not a type or value error.

The sentinel error values in the program are minimized and immediately wrapped with errors.Wrap when an error occurs, thus converting the error into an opaque error.

Finally, if you need to check, use errors.Cause to recover the underlying error.