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

Conrad Irwin in Engineering on January 22nd, 2015

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:

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.

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 crashy.Crash() can find out the stacktrace that led to the failure.

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

This helpfully prints out not only the error message, but also the stacktrace:

*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.

_, 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 return errors.Wrap(err, 1).

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 panic() started.

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

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 io.EOF when the end of a file is reached. As errors now have stacktraces, using equality won’t work. An io.EOF error returned from one part of your code won’t be the same as an io.EOF returned from another. You can work around this using errors.Is.

_, 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 errors.New() when returning it to attach the current stacktrace:

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 errors.Is to detect the expected condition:

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.