Busy Developer Guide to Go: Error wrapping

Tomek Cejner
3 min readSep 13, 2022

--

Welcome to the follow-up article on a series of bite-sized introductions to Golang. I covered the basics of error handling in the last part.

Errors are values, do you remember? It comes with a nice perk since you can use any programming technique to deal with errors. Let’s see what you can do if an error occurs deep in the call hierarchy.

Let’s consider an example: main() calls readConfig() which in turn calls readFromFile(). It's is a simplified example of a pattern occurring in code: nested calls and errors happening on a low level.

Let’s use errors.Wrap() from Google's errors package.

func readFromFile() (string, error) {
data, err := os.ReadFile("wrong file name")
if err != nil {
return "", errors.Wrap(err, "readFromFile")
}
return string(data), nil
}

func readConfig() (string, error) {
data, err := readFromFile()
if err != nil {
return "", errors.Wrap(err, "readConfig")
}
// ...
return data, nil
}

func main() {
conf, err := readConfig()
if err != nil {
log.Printf("Cannot read: %v", err)
}
}

The output of this program says:

2022/03/16 00:37:54 Cannot read: readConfig: readFromFile: open wrong file name: no such file or directory

At each level where error is wrapped, you may add a message. A cause, or maybe just a function name or other identification of level of abstraction.

Retrieving root cause

If you need the bottommost error of the trace, use

errors.Cause(err)

to retrieve the first error in the stack. The function is safe, so it will return the error when provided with an error without wrapping.

Built-in wrapping

The second way of error wrapping, which allows a bit of flexibility, is to use:

err = fmt.Errorf("read file: %w", err)

Notice the %w format specifier, which substitutes with error's text value. Keep in mind that this form does not preserve stack traces.

Bonus perk: stack trace

Errors wrapped using errors.Wrap() function preserve call stack. To print it, use %+v format specifier:

log.Printf("Cannot read: %+v", err)

will print:

2022/07/22 18:51:54 Cannot read: open wrong file name: no such file or directory
readFromFile
awesomeProject/learnWrapping.readFromFile
/Code/learn/go/awesomeProject/learnWrapping/wrapping.go:13
awesomeProject/learnWrapping.readConfig
/Code/learn/go/awesomeProject/learnWrapping/wrapping.go:19
awesomeProject/learnWrapping.Main
/Code/learn/go/awesomeProject/learnWrapping/wrapping.go:28
main.main
/Users/tomek/Code/learn/go/awesomeProject/main.go:6

The stack trace can be retrieved in another, a bit obscure way. Errors containing stack traces implement the private stackTracer interface (it’s private because name starts with lowercase letter). But, this is Go and you can redeclare that interface in your code:

type stackTracer interface {
StackTrace() errors.StackTrace
}

and access each stack frames individually:

if sterr, ok := err.(stackTracer); ok {
log.Printf("Stack trace:")
for n, f := range sterr.StackTrace() {
fmt.Printf("%d: %s %n:%d\n", n, f, f, f)
}
}

To see:

2022/07/23 15:11:17 Stack trace:
0: wrapping.go readConfig:21
1: wrapping.go Main:32
2: main.go main:6
3: proc.go main:250
4: asm_arm64.s goexit:1259

Handle at the bottom, log at the top

Wrapping is useful technique for preserving context information and helps avoiding double log statements.

Let’s revisit example from top: in the fourth line, highlighted in bold, the filesystem reading error is logged. Then it is logged again by main() function, resulting in double logs.

func readFromFile() (string, error) {
data, err := os.ReadFile("wrong file name")
if err != nil {
log.Printf("readFromFile failed")
return "", errors.Wrap(err, "readFromFile")
}
return string(data), nil
}

func readConfig() (string, error) {
data, err := readFromFile()
if err != nil {
return "", errors.Wrap(err, "readConfig")
}
// ...
return data, nil
}

func main() {
conf, err := readConfig()
if err != nil {
log.Printf("Cannot read: %v", err)
}
}

In result we’ll see duplicated statement in the logs:

2022/09/13 14:22:41 Cannot read file: "dummyfile.txt"
2022/09/13 14:22:41 Cannot read: readConfig: readFromFile: open dummyfile.txt: no such file or directory

In the example above, combination of wrapped errors gave enough context to pinpoint the cause of error. In the first line, we got out-of-context statement, which tells nothing.

You can, of course, pass the Context downstream to each and every method, but I don’t believe the benefits will overweigh lack of clarity caused by pollution.

Complaint Corner

  • For stack traces, you need to use “official-external” library.
  • Programmatic stack trace retrieval is limited and requires the use of Go tricks (implicit interface redefined by your code).

--

--

Tomek Cejner

Software engineer ~20 years in business. Did variety of things including Java backend, native mobile iOS, Node.js. Currently working with Go.