Golang Error Handling — Best Practice in 2020

Golang has many advantages, its popularity explains this. But error handling in Go 1 is not very efficient , we have to write many verbose , inconvenient codes in daily development.
There’re several open source solutions to tackle this. Meanwhile, the Go team is improving this from both the language and the standard library aspects.
Today we will analyze the common issues, compare the solutions, and show the best practices by now (go 1.13).
Conclusion first: my preference is github.com/pkg/errors. The reason will be explained in detail below.
Problem
When programing in Go, we need to check the returned error and handle it, a simplest example looks like this:
import (
"database/sql"
"fmt"
)
func foo() error {
return sql.ErrNoRows
}
func bar() error {
return foo()
}
func main() {
err := bar()
if err != nil {
fmt.Printf("got err, %+v\n", err)
}
}
//Outputs:
// got err, sql: no rows in result setSometimes we need to do different processing based on the different types of error:
import (
"database/sql"
"fmt"
)
func foo() error {
return sql.ErrNoRows
}
func bar() error {
return foo()
}
func main() {
err := bar()
if err == sql.ErrNoRows {
fmt.Printf("data not found, %+v\n", err)
return
}
if err != nil {
// Unknown error
}
}
//Outputs:
// data not found, sql: no rows in result setIn practice, we often add additional context to error before its return. The context will help caller to understand what’ going on. for example, we can rewrite function foo :
func foo() error {
return fmt.Errorf("foo err, %v", sql.ErrNoRows)
}Then the err == sql.ErrNoRows condition will turn to false. In addition, the call stack is dropped when an error is returned, which is the most important diagnostic information. We need a more flexible way to deal with such issues.
Solutions
There are several solutions to resolve the problem. Wrapping error with these libraries will keep the accessibility of root error and complete call stack
1. github.com/pkg/errors
from Dave Cheney , this library has 3 key methods:
Wrapis used to wrap the underlying error, add contextual text information, and attach the call stack. Generally it is used to wrap calls to API from other people (standard library or third-party library).WithMessageis used to add contextual text information to underlying error without attaching call stack. Apply this method for “wrapped error”only. Note: Do not repeatWrap, it will record redundancy call stacksCausemethod is for determining the underlying error
Rewrite the example above with github.com/pkg/errors :
import (
"database/sql"
"fmt"
"github.com/pkg/errors"
)
func foo() error {
return errors.Wrap(sql.ErrNoRows, "foo failed")
}
func bar() error {
return errors.WithMessage(foo(), "bar failed")
}
func main() {
err := bar()
if errors.Cause(err) == sql.ErrNoRows {
fmt.Printf("data not found, %v\n", err)
fmt.Printf("%+v\n", err)
return
}
if err != nil {
// unknown error
}
}/*Output:data not found, bar failed: foo failed: sql: no rows in result set
sql: no rows in result set
foo failed
main.foo
/usr/three/main.go:11
main.bar
/usr/three/main.go:15
main.main
/usr/three/main.go:19
runtime.main
...*/If we use %v as format parameter, we’ll get an one-line output string, contains all contextual text in the order of call stack. If change the format parameter to %+v , we’ll get the complete call stack.
If you want to simply wrap error with attach call stack, no additional contextual text is needed, then use WithStack
func foo() error {
return errors.WithStack(sql.ErrNoRows)
}Note: When use Wrap, WithMessage or WithStack, if the err parameter is nil, then nil will be returned, which means that we don’t need to check err != nil condition before calling the method. Keeping the code simple
2. golang.org/x/xerrors
After listening the feedback from community, the Go team published a proposal to simplify error handling in Go 2. Go core team member Russ Cox partially implemented the proposal in golang.org/x/xerrors. It solves the same problem with a similar approach to github.com/pkg/errors, introduces a format verb : %w, and it use method Is to determine the underlying error.
import (
"database/sql"
"fmt"
"golang.org/x/xerrors"
)
func bar() error {
if err := foo(); err != nil {
return xerrors.Errorf("bar failed: %w", foo())
}
return nil
}
func foo() error {
return xerrors.Errorf("foo failed: %w", sql.ErrNoRows)
}
func main() {
err := bar()
if xerrors.Is(err, sql.ErrNoRows) {
fmt.Printf("data not found, %v\n", err)
fmt.Printf("%+v\n", err)
return
}
if err != nil {
// unknown error
}
}
/* Outputs:data not found, bar failed: foo failed: sql: no rows in result set
bar failed:
main.bar
/usr/four/main.go:12
- foo failed:
main.foo
/usr/four/main.go:18
- sql: no rows in result set
*/Comparing to github.com/pkg/errors, it has several disadvantages:
- Replace
Wrapwith format parameter: %w. Looks simplify the code, but this approach loses compile-time checking. If: %wis not the tail of format string(e.g., "foo : %w bar"), or colon is missing (e.g., "foo %w"), or the space between colon and percent sign is missing (e.g., "foo:%w"), the wrapping will fail without any warning - What’s more serious is that you have to check the condition
err != nilbefore callingxerrors.Errorf. This actually does not simplify the work of the developer at all
3. Built-in Error Wrapping support in Go 1.13
As of Go 1.13, some (not all) features of xerrors have been integrated into the standard library. It inherits all the shortcomings of xerrors and contributes an additional one☹️. Therefore, I recommend against using it at the moment
import (
"database/sql"
"errors"
"fmt"
)
func bar() error {
if err := foo(); err != nil {
return fmt.Errorf("bar failed: %w", foo())
}
return nil
}
func foo() error {
return fmt.Errorf("foo failed: %w", sql.ErrNoRows)
}
func main() {
err := bar()
if errors.Is(err, sql.ErrNoRows) {
fmt.Printf("data not found, %+v\n", err)
return
}
if err != nil {
// unknown error
}
}
/* Outputs:
data not found, bar failed: foo failed: sql: no rows in result set
*/Similar to the xerrors version. However, it does not support call stack output. And according to the official statement, there is no schedule for this. So github.com/pkg/errors is much better choice at the moment
Summing-up
With above comparison, I believe you have made your choice. Let me be clear. My selection order is 1> 2> 3
- If you are using
github.com/pkg/errors, keep it. There is no solution better than it for now - If you’ve already used
golang.org/x/xerrorsheavily, don’t switch to the built-in solution in haste, which doesn’t deserve it
To be fair, Go has been quite mature and robust in most aspects since its birth. Hesitation and sway rarely occurred during its evolution. but error handling is an exception.
Not to mention the widely complained if err != nil, even its improvement roadmap is so controversial. Actually the Go team have adjusted a proposal due to overwhelming objections.
Fortunately, the Go team is more willing to listen to the community than before, and they even built a feedback page specifically on this issue. I believe we will find a better solution eventually
The last note
Although we discussed how to wrap error efficiently and elegantly, but like other technologies, it should only be used where appropriate. It should not be regarded as a general principle.
Why? When user start using errors.Cause(err, sql.ErrNoRows) or xerrors.Is (err, sql.ErrNoRows), it means that sql.ErrNoRows , as an implementation detail, is exposed to the outside and it becomes part of the API.
If you are doing application development with some libraries , It’s acceptable.
However, If you are defining some public APIs , this issue becomes particularly important. Maybe a better approach is to define a basic error type, then derive different error instances from it, with error code attached
See also: Mastering Wire






