github.com/pkg/errors is a popular errors package that adds context to the failure path in a way that does not destroy the original value of the error. Dave Cheney explains in his blog post why it is not possible to add context and check error type with vanilla Go errors. But that was not possible till Go v1.13 was released. Now Go has built in support for errors wrapping in base library.

How both wrappers work

I will refer github.com/pkg/errors package as external package to distinguish if from new standard Go errors.

Errors warped using Wrap function and original error extracted using function called Cause with external library:

import "github.com/pkg/errors"

internal := errors.New("internal error")
// add additional context to an error
wrapped := errors.Wrap(internal, "wrapper")
// get original error
unwrapped := errors.Cause(wrapped)

In Go 1.13 you can do the same using %w formatting verb and Unwrap function:

import "errors"

internal := errors.New("internal error")
// add additional context to an error
wrapped := fmt.Errorf("wrapper: %w", internal)
// get original error
unwrapped := errors.Unwrap(wrapped)

Wrappers are not compatible

What if I need to use both wrappers in my code. Can I wrap an error using one wrapper and unwrap using different one? Answer to this question is NO. You cannot do that.

Internally standard and external libraries use different interfaces to check if error has parent. Incompatibility can be added on purpose to push developer refactor whole errors chain.

Let’s see if there is any functional differences between libraries.

Unwrapping if there is no base error

Unwrapping without base error is first case when libraries work differently.

External Cause function returns error itself if it does not wrap another error:

import "github.com/pkg/errors"

func TestUnwrapBare(t *testing.T) {
    err := errors.New("some error")
    unwrapped := errors.Cause(err)
    require.NotNil(t, unwrapped)
    assert.Equal(t, "some error", unwrapped.Error())
}

Standard Unwrap function returns nil if error does not wrap another error:

import "errors"

func TestUnwrapBare(t *testing.T) {
    err := errors.New("some error")
    unwrapped := errors.Unwrap(err)
    require.Nil(t, unwrapped)
}

Deep nesting

Sometimes it is necessary to add context to the error (wrap) multiple times. Is there any differences between libraries in that case?

Answer is YES. External Cause function checks recursively all nested errors and returns the last one while standard Unwrap function returns first one without recursion.

Error call stack

github.com/pkg/errors package records a stack trace at the point error created if it created using New, Errorf, Wrap, and Wrapf functions. Later call stack can be rendered with %+v formatting verb:

import (
    "fmt"
    "os"

    "github.com/pkg/errors"
)

func main() {
    f, err := os.Open("notes.txt")
    if err != nil {
        err = errors.Wrap(err, "Error opening file")
        fmt.Printf(" %+v", err)
    }
    defer f.Close()
}

This little program fails with open file error. Later that error is wrapped into another error that adds mode details and call stack.

Printing with verb %+v returns full error call-stack:

 open notes.txt: no such file or directory
Error opening file
main.main
        /Users/dharnitski/go/src/github.com/dharnitski/go-new-errors/main.go:13
runtime.main
        /usr/local/Cellar/go/1.13/libexec/src/runtime/proc.go:203
runtime.goexit
        /usr/local/Cellar/go/1.13/libexec/src/runtime/asm_amd64.s:1357

Standard library does not track call-stack and prints much less information.

import (
    "fmt"
    "os"
)

func main() {
    f, err := os.Open("notes.txt")
    if err != nil {
        err = fmt.Errorf("Error opening file: %w", err)
        fmt.Printf(" %+v", err)
    }
    defer f.Close()
}

Console output for standard library:

 Error opening file: open notes.txt: no such file or directory

Source code for this post available on github.

Happy wrapping!