loading...
Cover image for An Error Wrapping Strategy for Go

An Error Wrapping Strategy for Go

chuck_ha profile image Chuck Ha ・3 min read

Does this look output familiar?

error running program: error parsing yaml: error opening file: error no such file "somefile.yaml"

The above is a pseudo-stack trace that you've likely encountered. It's better than just getting error no such file "somefile.yaml" as your error since this at least gives a bit of context as to where the error came from. But it's not great. It's hard to trace the code and not very pretty to read. The code that generated this error might look like this:

func main() {
    if err := run(); err != nil {
        fmt.Printf("error running program: %v\n", err)
    }
}

func run() error {
    if err := parseYAML("somefile.yaml"); err != nil {
        return fmt.Errorf("error parsing yaml: %v\n", err)
    }
    return nil
}

func parseYAML(file string) error {
    _, err := os.Open(file)
    if err != nil {
        return fmt.Errorf("error opening file: %v", err)
    }
    return nil
}

Link to the go playground with the code above.

This method is both prone to programmer error and it can still be a pain to trace the code without line numbers and file information. The improvement that can be made to this code is to record the stack trace!

The code above should look more like this after this improvement:

func main() {
    if err := run(); err != nil {
        fmt.Printf("encountered an error: %v\n", err)
        if stackerr, ok := err.(StackTrace); ok {
            fmt.Println(string(stackerr.StackTrace()))
        }
    }
}

func run() error {
    return parseYAML("somefile.yaml")
}

// A local error type that holds a stack trace
type Error struct {
    Err   error
    Stack []byte
}
func (e *Error) Error() string {
    return e.Err.Error()
}
func (e *Error) StackTrace() []byte {
    return e.Stack
}

type StackTrace interface {
    StackTrace() []byte
}

func parseYAML(file string) error {
    _, err := os.Open(file)
    if err != nil {
        return &Error{
            // keep the original error that was found
            Err: err,
            // Modify it with the stack trace
            Stack: debug.Stack(),
        }
    }
    return nil
}

Link to the go playground with the code above

Now the output specifies exactly what error occurred and where it occurred.

The rules for this refactoring are pretty straightforward:

  1. Wrap errors at the edge of your program.
  2. Print the stack at the top level.

Rule 1

Technically, this rule should be "wrap any error that doesn't already have a stack trace" but since there is no guarantee that libraries will add a stack trace to the errors they return it is easier to assume no library will add a stack trace and to always add one. The standard library is no exception to this rule.

Rule 2

Printing or logging the stack is very important, especially when the system/environment that generated the stack is unaccessible. In the open source world it's very common to get an issue where the submitter copy and pastes the output of the command they ran and the error they encountered. If your error doesn't have a stack trace, good luck trying to get someone to asynchronously rerun your program or a custom binary or with a special flag under the same circumstance. Some people do and that's great, but it would be fantastic if this information were available on every issue.

If you're looking for a library that does this for you, look no further than https://github.com/pkg/errors.

Discussion

pic
Editor guide