In the last post, I mentioned how a new approach to errors in Go differs from what used to. I also pointed out that it's not compatible with pkg/errors package. Now, I would like to show how you can actually make them kinda work together and migrate towards Go 1.13 approach.

Background

As I described in the previous post, starting from Go 1.13 version you can wrap errors with some additional context, while still being able to access the original, root cause. This has been available for some time already, with community libraries, including probably the most popular - pkg/errors. Unfortunately, the mechanism working in the new standard library and the 3rd party solution are not compatible, and wrapping error with one is not handled correctly by the other (and vice versa).

There have been some discussions in pkg/errors repo in one of the PRs, but the library maintainers suggested that they have no intentions of supporting this change and suggest using other solutions, eg. its forks.

The problem

Note For the purpose of this example I will use my own slomek/mappy as an example of 3rd party library, which is by no means completed, but it's good enough to draw the picture, so please bear with me for a while.

This situation causes a rather serious, and unfortunate problem to take place. Imagine that your application uses some external library libX to do its job. Since the library is completed (does its job perfectly and there is no need to extend it), nobody is working on it anymore. You are an eager Gopher that wants to introduce all the latest features of the language to your app, and choose to start using 1.13 error chain functionality.

Soon you start to realize that it's not that easy, as you realize that you rely on an exact root error in one of the execution paths, and the error is returned (and wrapped) by libX.

var p Person
if err := mappy.Unmarshal(data, &p); err != nil {
    if errors.Is(err, mappy.ErrMapUnmarshal) {
        fmt.Println("I'm so sorry, it's my fault - bad input data!")
        os.Exit(1)
    }

    fmt.Printf("slomek/mappy is crappy! Good input data, but returns error: %v\n", err)
    os.Exit(1)
}

The library, though, was completed way before Go 1.13, and it uses pkg/errors for wrapping errors, making it unwrappable for standard library solution:

// github.com/slomek/mappy/unmarshal.go
func Unmarshal(m map[string]string, data interface{}) (err error) {
    ...
        err = errors.Wrapf(ErrMapUnmarshal, "%v", r)
    ...
}()

This makes you so sad when you realize that your code will not work as you wanted to, and you will probably stay with pkg/errors for this reason.

$ go run main.go
slomek/mappy is crappy! Good input data, but returns error: reflect: call of reflect.Value.SetString on int Value: failed to unmarshal map into struct
exit status 1

What if you add another library to your app, which is using a standard library approach? Will you really be forced to support both unwrapping patterns, and remember when you should use one, and when the other?

Modules to the rescue

No! There is a solution, and it is really simple to execute, as long as you know just a bit about Go modules. With an assumption that you want to introduce all the new language features to your app, you probably have already introduced it to your code. In the case of my example application, the go.mod file looks as follows:

module github.com/slomek/golang-examples/misc/errors/go113migrate

go 1.13

require (
    github.com/pkg/errors v0.8.1 // indirect
    github.com/slomek/mappy v0.1.0
)

As you can see, I'm using my slomek/mappy library directly, which then forces me to use pkg/errors. As we've already seen, that indirect library should not feel welcome in our code because of its incompatibility with the standard library. We would much rather use one of its forks, which do provide the same mechanism as 1.13 language version. One of the forks is friendsofgo/errors which supports both approaches and would be perfect here. We would really love to see slomek/mappy maintainers to switch to that fork, but they are very stubborn and don't want that...

Instead of starting to yell at them on Github or Twitter, you can see at the possibilities that modules provide. You can actually force the library to use the fork without changing its code! All you need is a replace directive in your go.mod! This way you can say that whenever you see github.com/pkg/errors import, you want the compiler to use a completely different code by providing an aliased import path with a specific version that should be used:

// go.mod
...
replace github.com/pkg/errors => github.com/friendsofgo/errors v0.9.2

If you now run the app again, you can see that the error is recognized and handled with grace:

$ go run main.go
I'm so sorry, it's my fault - bad input data!
exit status 1

Time to celebrate, am I right?

Summary

As you can see, with just a tiny tweak in your own application code you can make a 3rd library, legacy-like code be compatible with most recent changes to the language. You can now start to migrate your code to Go 1.13 error chain without worrying that any dependency still uses an incompatible pkg/errors package.

Full source code of the example application is available on Github.

Versions

  • go - go version go1.13 linux/amd64
  • github.com/pkg/errors - v0.8.1
  • github.com/friendsofgo/errors - v0.9.2

Shoutout

Special thanks to my workmate, Tobiasz Heller, for inspiring me to write this post, as he saw a lot articles about the new errors approach, but is yet to see one about migrating existing code to it. I recommend following him on Github and if you happen to have a chance to see any of his talks, do not miss them. We're talking about a top-notch quality, hoping to see him at an international conference soon (no pressure, buddy 😆).