One of the strangest things Go developers can do is to use a specific error type rather than an interface as the return type of a function. If you do, remember this is a very bad idea with unpredictable results.
Let's take a look at this block of code:
type myError struct{}
func (c *myError) Error() string {
return "Unexpected Error."
}
func run() ([]byte, *myError) {
return nil, nil
}
func main() {
var err error
if _, err = fail(); err != nil {
log.Fatal("We are failed")
}
log.Println("Everything is OK")
}
If we run this program, we suddenly get the error "We are failed". Why does this happen? The reason is that the run function uses a particular implementation of the error and not the interface. In this case the nil pointer of type myError is stored inside the variable err. And this is not the same as an empty interface value of type err.
Error handling is more of an engineering thought on a macro level. So what does "error handling" mean? An error is stopped by the error-handling function, the error is logged with the full context, and the error is checked for severity. Based on the severity and recoverability, a decision is made whether to recover, continue, or terminate the script.
One problem is that not all functions can handle the error. Simply not all functions keep an error log. What happens when an error is passed back up the call stack and cannot be handled by the function that received it? The error must be wrapped in context so that the function that ultimately handles it can do so properly.
There are two options for wrapping additional context around the error. You can use Dave Cheney's third-party errors package, or you can use the standard library support found in the errors and fmt packages. Whatever I decide, it is important to annotate errors for sufficient context to help identify and fix problems. Both at runtime and afterwards.
An example of using a third party package:
package main
import (
"fmt"
"github.com/pkg/errors"
)
type MainError struct {
State int
}
func (c *MainError) Error() string {
return fmt.Sprintf("Main Error, State: %d", c.State)
}
func main() {
if err := first(10); err != nil {
switch v := errors.Cause(err).(type) {
case *MainError:
fmt.Println("Custom Main Error:", v.State)
default:
fmt.Println("Default Error")
}
}
fmt.Printf("%v\n", err)
}
func first(i int) error {
if err := second(i); err != nil {
return errors.Wrapf(err, "secondCall(%d)", i)
}
return nil
}
func second(i int) error {
return &MainError{99}
}
There will be following output:
Custom Main Error: 99
second(10): Main Error, State: 99
What I like about this package is the errors.Wrap and errors.Cause functions. They make the code more readable.
If we were writing code without using the library, we would need to create these functions ourselves:
func Cause(err error) error {
root := err
for {
if err = errors.Unwrap(root); err == nil {
return root
}
root = err
}
}
Thus, it is very important to approach the question of error handling with great care. It is necessary to pass the full context to functions and pay attention to the return type.