Error handling in golang 1.13
TL;DR
errors
standard library package in golang 1.13 cannot be alternatives ofpkg/errors
andxerrors
.- Use
xerrors
for error handling for new golang projects. - No need to switch from
pkg/errors
toxerrors
.
Motivation
golang is a very simple language and that’s the core philosophy of its design. Along with the design, standard libraries are also simple and don’t do too many things. However, the features of error
standard library is not enough for developments. It cannot traverse stack traces and developers are difficult to debug if we only use the library for error handling. To introduce stack traces, we use pkg/errors
or xerrors
.
From golang 1.13, the error
standard library finally incorporated some features implemented in xerrors
. So, let me summarize my conclusions whether we can migrate from pkg/errors
to the errors
standard library or not.
Prerequisite
- The target golang version for this article is 1.13
Migration from pkg/errors to errors
Unfortunately, I don’t think we can migrate pkg/errors
to the error
standard library because the error
package in golang 1.13 cannot provide stack traces. The following code tries to wrap error chains and output by fmt.Printf()
with %+v
.
package main
import (
"errors"
"fmt"
"os"
)
func firstFunc() error {
err := secondFunc()
if err != nil {
err = fmt.Errorf("Wrap in firstFunk: %w", err)
return err
}
return nil
}
func secondFunc() error {
err := thirdFunc()
if err != nil {
err = fmt.Errorf("Wrap in secondFunk: %w", err)
return err
}
return nil
}
func thirdFunc() error {
return errors.New("Error produced by errors.New() in thirdFunc")
}
func main() {
err := firstFunc()
if err != nil {
fmt.Println(`### Outputs for fmt.Printf("[ERROR]: %+v\n", err)`)
fmt.Printf("[ERROR]: %+v\n\n", err)
fmt.Println(`### Outputs for fmt.Printf("[ERROR]: %+v\n", errors.Unwrap(err))`)
fmt.Printf("[ERROR]: %+v\n", errors.Unwrap(err))
os.Exit(1)
}
fmt.Println("Success")
os.Exit(0)
}
You can run this code on Go Playground. The result of the above code is as follows. This is better than the behavior of the error
packages before 1.13 though what we really want the errors
package to incorporate is a stack tracing.
### Outputs for fmt.Printf("[ERROR]: %+v\n", err)
[ERROR]: Wrap in firstFunk: Wrap in secondFunk: Error produced by errors.New() in thirdFunc
### Outputs for fmt.Printf("[ERROR]: %+v\n", errors.Unwrap(err))
[ERROR]: Wrap in secondFunk: Error produced by errors.New() in thirdFunc
Stack traces with pkg/errors
If we run the same code with pkg/errors
we can gain stack traces.
package main
import (
"fmt"
"os"
"github.com/pkg/errors"
)
func firstFunc() error {
err := secondFunc()
if err != nil {
err = errors.Wrap(err, "Wrap in firstFunk: ")
return err
}
return nil
}
func secondFunc() error {
err := thirdFunc()
if err != nil {
err = errors.Wrap(err, "Wrap in secondFunk: ")
return err
}
return nil
}
func thirdFunc() error {
return errors.New("Error produced by errors.New() in thirdFunc")
}
func main() {
err := firstFunc()
if err != nil {
fmt.Println(`### Outputs for fmt.Printf("[ERROR]: %+v\n", err)`)
fmt.Printf("[ERROR]: %+v\n\n", err)
fmt.Println(`### Outputs for fmt.Printf("[ERROR]: %+v\n", errors.Cause(err))`)
fmt.Printf("[ERROR]: %+v\n", errors.Cause(err))
os.Exit(1)
}
fmt.Println("Success")
os.Exit(0)
}
You can run the above code on Go Playground and get the result below. As you can see, stack traces are listed.
### Outputs for fmt.Printf("[ERROR]: %+v\n", err)
[ERROR]: Error produced by errors.New() in thirdFunc
main.thirdFunc
/tmp/sandbox141164394/prog.go:29
main.secondFunc
/tmp/sandbox141164394/prog.go:20
main.firstFunc
/tmp/sandbox141164394/prog.go:11
main.main
/tmp/sandbox141164394/prog.go:33
runtime.main
/usr/local/go-faketime/src/runtime/proc.go:203
runtime.goexit
/usr/local/go-faketime/src/runtime/asm_amd64.s:1373
Wrap in secondFunk:
main.secondFunc
/tmp/sandbox141164394/prog.go:22
main.firstFunc
/tmp/sandbox141164394/prog.go:11
main.main
/tmp/sandbox141164394/prog.go:33
runtime.main
/usr/local/go-faketime/src/runtime/proc.go:203
runtime.goexit
/usr/local/go-faketime/src/runtime/asm_amd64.s:1373
Wrap in firstFunk:
main.firstFunc
/tmp/sandbox141164394/prog.go:13
main.main
/tmp/sandbox141164394/prog.go:33
runtime.main
/usr/local/go-faketime/src/runtime/proc.go:203
runtime.goexit
/usr/local/go-faketime/src/runtime/asm_amd64.s:1373
### Outputs for fmt.Printf("[ERROR]: %+v\n", errors.Cause(err))
[ERROR]: Error produced by errors.New() in thirdFunc
main.thirdFunc
/tmp/sandbox141164394/prog.go:29
main.secondFunc
/tmp/sandbox141164394/prog.go:20
main.firstFunc
/tmp/sandbox141164394/prog.go:11
main.main
/tmp/sandbox141164394/prog.go:33
runtime.main
/usr/local/go-faketime/src/runtime/proc.go:203
runtime.goexit
/usr/local/go-faketime/src/runtime/asm_amd64.s:1373
Stack traces with xerrors
This time, let’s use xerrors
to gain stack traces.
package main
import (
"fmt"
"os"
"golang.org/x/xerrors"
)
func firstFunc() error {
err := secondFunc()
if err != nil {
err = xerrors.Errorf("Wrap in firstFunk: %w", err)
return err
}
return nil
}
func secondFunc() error {
err := thirdFunc()
if err != nil {
err = xerrors.Errorf("Wrap in secondFunk: %w", err)
return err
}
return nil
}
func thirdFunc() error {
return xerrors.New("Error produced by errors.New() in thirdFunc")
}
func main() {
err := firstFunc()
if err != nil {
fmt.Println(`### Outputs for fmt.Printf("[ERROR]: %+v\n", err)`)
fmt.Printf("[ERROR]: %+v\n\n", err)
fmt.Println(`### Outputs for fmt.Printf("[ERROR]: %+v\n", xerrors.Unwrap(err))`)
fmt.Printf("[ERROR]: %+v\n", xerrors.Unwrap(err))
os.Exit(1)
}
fmt.Println("Success")
os.Exit(0)
}
Again, you can execute the above code on Go Playground and get the result below. xerrors
also provides stack traces.
### Outputs for fmt.Printf("[ERROR]: %+v\n", err)
[ERROR]: Wrap in firstFunk:
main.firstFunc
/tmp/sandbox583951390/prog.go:13
- Wrap in secondFunk:
main.secondFunc
/tmp/sandbox583951390/prog.go:22
- Error produced by errors.New() in thirdFunc:
main.thirdFunc
/tmp/sandbox583951390/prog.go:29
### Outputs for fmt.Printf("[ERROR]: %+v\n", xerrors.Unwrap(err))
[ERROR]: Wrap in secondFunk:
main.secondFunc
/tmp/sandbox583951390/prog.go:22
- Error produced by errors.New() in thirdFunc:
main.thirdFunc
/tmp/sandbox583951390/prog.go:29
Diffs between pkg/errors, xerrors, and errors
stack traces
pkg/errors
### Outputs for fmt.Printf("[ERROR]: %+v\n", errors.Cause(err))
[ERROR]: Error produced by errors.New() in thirdFunc
main.thirdFunc
/tmp/sandbox141164394/prog.go:29
main.secondFunc
/tmp/sandbox141164394/prog.go:20
main.firstFunc
/tmp/sandbox141164394/prog.go:11
main.main
/tmp/sandbox141164394/prog.go:33
runtime.main
/usr/local/go-faketime/src/runtime/proc.go:203
runtime.goexit
/usr/local/go-faketime/src/runtime/asm_amd64.s:1373
- I like this style of stack traces
xerrors
### Outputs for fmt.Printf("[ERROR]: %+v\n", xerrors.Unwrap(err))
[ERROR]: Wrap in secondFunk:
main.secondFunc
/tmp/sandbox583951390/prog.go:22
- Error produced by errors.New() in thirdFunc:
main.thirdFunc
/tmp/sandbox583951390/prog.go:2
- I don’t like this style of stack traces
errors
The errors
does not support stack traces in 1.13.
Error wrapping
pkg/errors
errors.Wrap(err, "this is an error example")
errors.Wrap()
returnsnil
whenerr
isnil
.
xerrors
xerrors.Errorf("this is an error example: %w", err)
: %w
must be placed at the end of a string forxerrors.Errorf()
- if you don’t follow this rule, a wrapping is ignored without any warning. This is a disadvantages to use
xerrors
package for error handling. We may need to introduce a lint to check violations for this rule.
- if you don’t follow this rule, a wrapping is ignored without any warning. This is a disadvantages to use
xerrors.Errorf()
returns errors whenerr
isnil
.nil check is necessary before
xerrors.Errorf()
. Otherwise, error chains won’t work correctly.package main import ( "fmt" "os" "golang.org/x/xerrors" ) func correct_func() error { var err error = nil if err != nil { return xerrors.Errorf("this is an error example: %w", err) } return nil } func wrong_func() error { var err error = nil return xerrors.Errorf("this is an error example: %w", err) // this part does not return nil } func main() { err := correct_func() if err != nil { fmt.Printf("Error in correct_func(): %+v", err) os.Exit(1) } err = wrong_func() if err != nil { // this part will be ignored due to the lack of nil check in wrong_func() fmt.Printf("Error in wrong_func(): %+v", err) os.Exit(1) } fmt.Println("Succeeded!!") os.Exit(0) } //> go run main.go //Error in wrong_func(): this is an error example: %!w(<nil>): // main.wrong_func // /tmp/sandbox339401097/prog.go:20
errors
errors.Errorf("this is an error example: %w", err)
: %w
can be placed anywhere forerrors.Errorf()
Unwrap method(traversing error cause)
pkg/errors
errors.Cause(err)
xerrors
xerrors.Unwrap(err)
errors
errors.Unwrap(err)
Value comparison
pkg/errors
err = errors.Cause(err)
if err == ErrorA {
// implementation for ErrorA
}
xerrors
if xerrors.Is(err, ErrorA) {
// implementation for ErrorA
}
errors
if xerrors.Is(err, ErrorA) {
// implementation for ErrorA
}
Type comparison
pkg/errors
if errA, ok := err.(*ErrorA); ok {
// implementation for ErrorA
}
xerrors
var errA *ErrorA
if xerrors.As(err, &errA); ok {
// implementation for ErrorA
}
errors
var errA *ErrorA
if errors.As(err, &errA); ok {
// implementation for ErrorA
}
Conclusion
In golang 1.13, the error
standard pacakge cannot be alternatives of pkg/errors
and xerrors
due to the lack of a stack tracing feature. If you use pkg/errors
and xerrors
for your products, you don’t have to be rush to migrate to the standard library at this moment. In addition, you don’t have to migrate from pkg/errors
to xerrors
because there are clear differences between syntaxes, stack trace behavior and the return value of their wrapping methods. I personally like using pkg/errors
and will continue to use the package for my private projects.