極光日記

【Go】Differences in Stack Trace Output Between Error Libraries

Created:

In this article, I compare how stack traces are printed when handling errors in Go using the pkg/errors and cockroachdb/errors libraries.

The README of cockroachdb/errors states:

This library aims to be used as a drop-in replacement to github.com/pkg/errors and Go’s standard errors package.

While cockroachdb/errors is intended as a replacement for pkg/errors, actual usage shows that stack trace output is not identical between the two libraries. Below are the results of experiments highlighting the differences. In all cases, stack traces were printed using fmt.Printf("%+v\n", err) for both libraries.

pkg/errors

Wrapping Only at the Error Origin

“Wrapping only at the error origin” refers to the following pattern:

func funcD1() error {
	err := funcD2()
	return err
}

func funcD2() error {
	_, err := os.Open("text.txt")

	return errors.Wrap(err, "funcD2")
}

Here, funcD2 is the point where the error occurs and is wrapped, while funcD1 simply returns the error without wrapping it again.

In this case, the stack trace looks like this:

open text.txt: no such file or directory
funcD2
main.funcD2
	/home/newvm/ghq/gitlab.com/my-study-etc-group/go-error-study/main.go:78
main.funcD1
	/home/newvm/ghq/gitlab.com/my-study-etc-group/go-error-study/main.go:71
main.main
	/home/newvm/ghq/gitlab.com/my-study-etc-group/go-error-study/main.go:28
runtime.main
	/home/newvm/go/pkg/mod/golang.org/[email protected]/src/runtime/proc.go:267
runtime.goexit
	/home/newvm/go/pkg/mod/golang.org/[email protected]/src/runtime/asm_amd64.s:1650

Wrapping at Every Level

“Wrapping at every level” refers to the following pattern:

func funcE1() error {
	err := funcE2()
	return errors.Wrap(err, "funcE1")
}

func funcE2() error {
	_, err := os.Open("text.txt")

	return errors.Wrap(err, "funcE2")
}

In this case, not only funcE2, where the error occurs, but also funcE1 wraps the error.

The resulting stack trace is as follows:

open text.txt: no such file or directory
funcE2
main.funcE2
	/home/newvm/ghq/gitlab.com/my-study-etc-group/go-error-study/main.go:89
main.funcE1
	/home/newvm/ghq/gitlab.com/my-study-etc-group/go-error-study/main.go:82
main.main
	/home/newvm/ghq/gitlab.com/my-study-etc-group/go-error-study/main.go:33
runtime.main
	/home/newvm/go/pkg/mod/golang.org/[email protected]/src/runtime/proc.go:267
runtime.goexit
	/home/newvm/go/pkg/mod/golang.org/[email protected]/src/runtime/asm_amd64.s:1650
funcE1
main.funcE1
	/home/newvm/ghq/gitlab.com/my-study-etc-group/go-error-study/main.go:83
main.main
	/home/newvm/ghq/gitlab.com/my-study-etc-group/go-error-study/main.go:33
runtime.main
	/home/newvm/go/pkg/mod/golang.org/[email protected]/src/runtime/proc.go:267
runtime.goexit
	/home/newvm/go/pkg/mod/golang.org/[email protected]/src/runtime/asm_amd64.s:1650

The stack trace is duplicated, making it harder to read.

cockroachdb/errors

Wrapping Only at the Error Origin

As with pkg/errors, “wrapping only at the error origin” looks like this:

func funcB1() error {
	err := funcB2()
	return err
}

func funcB2() error {
	_, err := os.Open("text.txt")

	return errors.Wrap(err, "funcB2")
}

Here, funcB2 is the origin of the error and performs the wrapping, while funcB1 simply returns the error.

The resulting stack trace is:

funcB2: open text.txt: no such file or directory
(1) attached stack trace
  -- stack trace:
  | main.funcB2
  | 	/home/newvm/ghq/gitlab.com/my-study-etc-group/go-error-study/main.go:56
  | main.funcB1
  | 	/home/newvm/ghq/gitlab.com/my-study-etc-group/go-error-study/main.go:49
  | main.main
  | 	/home/newvm/ghq/gitlab.com/my-study-etc-group/go-error-study/main.go:18
  | runtime.main
  | 	/home/newvm/go/pkg/mod/golang.org/[email protected]/src/runtime/proc.go:267
  | runtime.goexit
  | 	/home/newvm/go/pkg/mod/golang.org/[email protected]/src/runtime/asm_amd64.s:1650
Wraps: (2) funcB2
Wraps: (3) open text.txt
Wraps: (4) no such file or directory
Error types: (1) *withstack.withStack (2) *errutil.withPrefix (3) *fs.PathError (4) syscall.Errno

Wrapping at Every Level

“Wrapping at every level” refers to the following pattern:

func funcC1() error {
	err := funcC2()
	return errors.Wrap(err, "funcC1")
}

func funcC2() error {
	_, err := os.Open("text.txt")

	return errors.Wrap(err, "funcC2")
}

In this case, both funcC2 (where the error occurs) and funcC1 wrap the error.

The resulting stack trace is:

funcC1: funcC2: open text.txt: no such file or directory
(1) attached stack trace
  -- stack trace:
  | main.funcC1
  | 	/home/newvm/ghq/gitlab.com/my-study-etc-group/go-error-study/main.go:61
  | [...repeated from below...]
Wraps: (2) funcC1
Wraps: (3) attached stack trace
  -- stack trace:
  | main.funcC2
  | 	/home/newvm/ghq/gitlab.com/my-study-etc-group/go-error-study/main.go:67
  | main.funcC1
  | 	/home/newvm/ghq/gitlab.com/my-study-etc-group/go-error-study/main.go:60
  | main.main
  | 	/home/newvm/ghq/gitlab.com/my-study-etc-group/go-error-study/main.go:23
  | runtime.main
  | 	/home/newvm/go/pkg/mod/golang.org/[email protected]/src/runtime/proc.go:267
  | runtime.goexit
  | 	/home/newvm/go/pkg/mod/golang.org/[email protected]/src/runtime/asm_amd64.s:1650
Wraps: (4) funcC2
Wraps: (5) open text.txt
Wraps: (6) no such file or directory
Error types: (1) *withstack.withStack (2) *errutil.withPrefix (3) *withstack.withStack (4) *errutil.withPrefix (5) *fs.PathError (6) syscall.Errno

Unlike pkg/errors, cockroachdb/errors detects duplicated stack frames and shortens the output using markers such as [...repeated from below...]. As a result, even when wrapping at every level, the output is less verbose and easier to read.

Additional Notes

The repository used for these experiments is available here: