New Errors in Go
Dave Cheney’s github.com/pkg/errors
has, for several years, been one of the main tools in getting sensible error messages out of Go services.
Looking at what happens without any additional error enrichment, it’s clear why…
package main
import (
"fmt"
"os"
)
func readFile() error {
f, err := os.Open("myfile.txt")
if err != nil {
return err
}
defer f.Close()
return nil
}
func subError() error {
if err := readFile(); err != nil {
return err
}
return nil
}
func main() {
err := subError()
fmt.Printf("err = %+v\n", err)
}
$ go run testing.go
err = open myfile.txt: no such file or directory
Nothing to indicate where the error happened…
github/pkg/errors output
Comparing that with the output, when wrapping errors with the errors
package:
package main
import (
"fmt"
"os"
"github.com/pkg/errors"
)
func readFile() error {
f, err := os.Open("myfile.txt")
if err != nil {
return errors.Wrapf(err, "failed to open file")
}
defer f.Close()
return nil
}
func subError() error {
if err := readFile(); err != nil {
return errors.WithMessage(err, "subError got an error from readFile")
}
return nil
}
func main() {
err := subError()
fmt.Printf("err (%#v) = %+v\n", errors.Cause(err), err)
}
$ go run testing.go
err (&os.PathError{Op:"open", Path:"myfile.txt", Err:0x2}) = open myfile.txt: no such file or directory
failed to open file
main.readFile
/Users/kevin/Source/blog/_posts/testing.go:13
main.subError
/Users/kevin/Source/blog/_posts/testing.go:20
main.main
/Users/kevin/Source/blog/_posts/testing.go:27
runtime.main
/usr/local/Cellar/go/1.12.9/libexec/src/runtime/proc.go:200
runtime.goexit
/usr/local/Cellar/go/1.12.9/libexec/src/runtime/asm_amd64.s:1337
subError got an error from readFile
It’s clear whereabouts in the code that the error happened, there’s a handy traceback, it’s also possible to get the “cause” the type of the error that occurred.
golang.org/x/exp/errors/fmt
Coming soon to Go, is an error enrichment mechanism in the standard library, and it’s available today!
Unlike the try proposal, the “Go 2 Error Inspection” proposal has been accepted as an enhancement to the language.
How does this affect code?
package main
import (
"os"
"golang.org/x/exp/errors/fmt"
)
func readFile() error {
f, err := os.Open("myfile.txt")
if err != nil {
return fmt.Errorf("failed to open file: %w", err)
}
defer f.Close()
return nil
}
func subError() error {
if err := readFile(); err != nil {
return fmt.Errorf("subError got an error from readFile: %w", err)
}
return nil
}
func main() {
err := subError()
fmt.Printf("err = %+v\n", err)
}
Note that the golang.org/x/exp/errors/fmt
package is used in place of the stdlib fmt
package, and it uses the %w
formatting directive to indicate it should wrap an error.
Executing this version gets:
$ go run testing.go
err = subError got an error from readFile:
main.subError
/Users/kevin/Source/blog/_posts/testing.go:20
- failed to open file:
main.readFile
/Users/kevin/Source/blog/_posts/testing.go:12
- open myfile.txt: no such file or directory
This is very similar to the github.com/pkg/errors
output, one nice change is being able to use Errorf
for each response, rather than having to use Wrapf
and WithMessage
, but I feel that %w
isn’t as obvious when reading the code to indicate that it’s returning a wrapped error.
Getting the error cause isn’t as easy, but there are utility functions for determining whether or not the underlying (wrapped) error is a specific error type.
Switching to use the new stdlib version is easy enough, except maybe for the Cause
wrinkle, it’s worth reading the doc to understand why Cause
is omitted.
I see that github.com/pkg/errors
is now in maintenance mode, the new errors
functionality should land in Go 1.13.