3
minute read

How to get stacktraces from errors in Golang with go-errors/errors

Conrad Irwin

It’s been 6 months since we released bugsnag-go, allowing you to send Go error reports to Bugsnag. It’s high time to share the technology we built to make it work: the go-errors/errors library.

By default in Go, errors don’t have a stacktrace. In fact, an error is a very simple interface:

-- CODE language-go --
interface error {
 Error() string
}

In other words, the only thing you can get out of that error is the string that describes what went wrong.

This simplicity provides great power for handling expected errors, as described by Rob Pike last week in Errors are values.Unfortunately, it doesn’t provide everything you need for debugging  problems that are unexpected. This is where the go-errors/errors package comes in.

Getting the stacktrace

The most useful piece of context for tracking down an unexpected failure is the stacktrace so it’s a good idea to print it out. This lets you trace through the program and see why it ended up in the state it did.

Errors in Go are usually returned, but in order to associate a stacktrace with an error, you’ll have to do that at the point it’s created using go-errors.

-- CODE language-go --
package crashy

import "github.com/go-errors/errors"

func Crash() error {
   return errors.Errorf("this function is supposed to crash");
}

Now any function that calls [CODE]crashy. Crash()[/CODE] can find out the stacktrace that led to the failure.

-- CODE language-go --
err := crashy.Crash()
if err != nil {
   fmt.Println(err.(*errors.Error).ErrorStack())
}

This helpfully prints out not only the [CODE]error[/CODE] message, but also the stacktrace:

-- CODE language-plaintext --
*errors.errorString this function is supposed to crash
/0/go/src/github.com/go-errors/errors/crashy/crashy.go:8 (0x41280)
   Crash: return errors.New(Crashed)
/0/go/src/github.com/go-errors/errors/example/main.go:11 (0x2026)
   main: err := crashy.Crash()

Wrapping existing errors

Most libraries in Go already return the simple, featureless error interface. In order to add a stacktrace to those existing errors, you can Wrap them using the errors library. This’ll give you the stacktrace at the point the error made its way into your code, which is usually good enough to track down the problem.

-- CODE language-go --
_, err := reader.Read(buff)
if err != nil {
   return errors.Wrap(err, 1)
}

The second parameter to Wrap allows you to control where the stacktrace appears to start. When set to 1 it will be the line that contains [CODE]return errors. Wrap(err, 1)[/CODE].

Recovering from panics

This section assumes you know about defer, panic and recover.

When Go is recovering from a panic, it calls your deferred functions with the stacktrace still intact. This means that it’s possible to get the stacktrace exactly as it was when the [CODE]panic()[/CODE] started.

To do this you can use [CODE]errors.Wrap[/CODE] inside your deferred handler as follows:

-- CODE language-go --
defer func() {
   if err := recover(); err != nil {
       fmt.Println(errors.Wrap(err, 2).ErrorStack())
   }
}()

Programming with errors

Using the errors package does have one small downside: you can’t compare errors using equality anymore.

For example, a common pattern in Go is to return [CODE]io.EOF[/CODE] when the end of a file is reached. As errors now have stacktraces, using equality won’t work. [CODE]Anio.EOF[/CODE] error returned from one part of your code won’t be the same as [CODE]anio.EOF[/CODE] returned from another. You can work around this using [CODE]errors.Is[/CODE].

-- CODE language-go --
_, err := reader.Read(buff)
if errors.Is(err, io.EOF) {
   return nil
}

If you want your package to expose errors for common conditions, you can do that using the following pattern. First create an error template at the top of your file, then call [CODE]errors.New()[/CODE] when returning it to attach the current stacktrace:

-- CODE language-go --
package crashy

import "github.com/go-errors/errors"

var ExpectedCrash = errors.Errorf("expected crash")

func Crash() error {
   return errors.New(ExpectedCrash)
}

Your callers can then use [CODE]errors.Is[/CODE] to detect the expected condition:

-- CODE language-go --
package main
import (
   "crashy"
   "github.com/go-errors/errors"
)

func main() {
   err := crashy.Crash()
   if errors.Is(err, crashy.ExpectedError) {
       return
   }
}

Summary

I hope you agree that having stacktraces makes it much easier to track down unexpected errors, and that having errors as values makes it easy to handle expected edge cases.

go-errors/errors was designed to give you the best of both worlds. Please let me know on Twitter how it works for you.

Bugsnag helps you proactively monitor and improve your software quality.