{ Soham Kamani }

About β€’ Blog β€’ Github β€’ Twitter

How to generate an error stack trace in Go πŸ₯ž

A common problem that many people (including me) have faced when programming in Go, is to pin point the source of an error. Other programming languages provide you with a stack trace to tell you where the error came from, but Go does not have this behavior by default.

In this article, we will discuss how to use the fmt, and the github.com/pkg/errors libraries to give us better error reporting.

Consider this example:

package main

import (
	"fmt"
	"errors"
)

func main() {
	result, err := caller1()
	if err != nil {
		fmt.Println("Error: ", err)
		return
	}
	fmt.Println("Result: ", result)
}

func caller1() (int, error) {
	err := caller2()
	if err != nil {
		return 0, err
	}
	return 1, nil
}

func caller2() error {
  doSomething()
	return caller3()
}


func caller3() error {
	return errors.New("failed")
}

Try it here

Running this program would output:

Error:  failed

Now, this does not really tell us much. What we really want to know is the cause of the error (which, in this case is the caller3 function), and where the error came from (which would be the stack trace leading up to caller3)

Using the standard library

We can use the fmt.Errorf function to wrap other errors and effectively generate a trace:

//caller1 and caller2 can be modified to wrap the errors with `fmt.Errorf` before returning them

func caller1() (int, error) {
	err := caller2()
	if err != nil {
		return 0, fmt.Errorf("[caller1] error in calling caller2: %v", err)
	}
	return 1, nil
}

func caller2() error {
	doSomething()
	err := caller3()
	if err != nil {
		return fmt.Errorf("[caller2] error in calling caller 3: %v", err)
	}
	return nil
}

Try it here

Running this would give you:

Error:  [caller1] error in calling caller2: [caller2] error in calling caller 3: failed

This time, the error is much more descriptive, and tells us the sequence of events that lead to the error. Wrapping errors in the format: ”[<name of the function>] <description of error> : <actual error>” gives us a consistent way to find its cause.

But what about custom error types?

Consider having an error type that also has an additional error code:

type CustomError struct {
  Code int
}

func (c *CustomError) Error() string {
  return fmt.Sprintf("Failed with code %d", c.Code)
}

If you wrap this error with fmt.Errorf, its original type will be lost, and you won’t be able to access the Code struct attribute, or even tell that the error is of type CustomError

The solution to this lies in the github.com/pkg/errors libraries errors.Wrap, and errors.Cause functions.

import (
	"fmt"
	"github.com/pkg/errors"
)

func main() {
	err := &CustomError{Code: 12}
	// lostErr := fmt.Errorf("failed with error: %v", err)
	// there is no way we can get back the `Code` attribute from `lostErr`

	wrappedErr := errors.Wrap(err, "[1] failed with error:")
	twiceWrappedError := errors.Wrap(wrappedErr, "[2] failed with error:")

  // The `errors.Cause` function returns the originally wrapped error, which we can then type assert to its original struct type
	if originalErr, ok := errors.Cause(twiceWrappedError).(*CustomError); ok {
		fmt.Println("the original error coed was : ", originalErr.Code)
	}	
}

Adding a trail to errors in Go is almost necessary for any medium to large application if you don’t want to lose your head debugging its cause. Using the β€œerrors” library lets you maintain the trail, while still retaining the benefits of inspecting the original error.


Like what I write? Join my mailing list, and I'll let you know whenever I write another post

Comments

Soham Kamani

Written by Soham Kamani, an author,and a full-stack developer who has extensive experience in the JavaScript ecosystem, and building large scale applications in Go. He is an open source enthusiast and an avid blogger. You should follow him on Twitter