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.