Featured image

What I wanted to do Link to heading

Today’s pkg/errors Link to heading

Package errors is a popular go error wrapping package. The StackTrace type represents the stack trace with an []Frame. Errors wrapped by package errors implicitly implement the stackTracer interface, which allows users to extract stack information.

type StackTrace []Frame  
type Frame uintptr

type stackTracer interface {  
    StackTrace() errors.StackTrace  
}

Simultaneously to this, there exist libraries that would like to extract stack frame information from errors, if it’s present. One example is the rollbar logging library, which allows logging errors with the stack trace that created them. The only way for a rollbar logging library to support pkg/errors is to import the library directly and type assert for a StackTrace function.

import "github.com/pkg/errors"
type stackTracer interface {  
    StackTrace() errors.StackTrace  
}
func logError(err error) {  
    if s, ok := err.(stackTracer); ok {  
        // ...  
    }  
}

To resolve some issues with pkg/errors I decided to fork it. Because of stackTracer’s implementation, my forked StackTrace function will not resolve when type asserted. Not only is my fork unsupported by the rollbar library, but I’m also forced to vendor and manage dependencies to pkg/errors.

What I actually did Link to heading

Forking pkg/errors Link to heading

To support the fork, the rollbar library is now required to also check for my stackTracer implementation.

import errors1 "github.com/pkg/errors"  
import errors2 "github.com/cep21/errors"

type stackTracer1 interface {  
    StackTrace() errors1.StackTrace  
}

type stackTracer2 interface {  
    StackTrace() errors2.StackTrace  
}

func logError(err error) {  
    if s, ok := err.(stackTracer1); ok {
        // ...
    }
    if s, ok := err.(stackTracer2); ok {  
        // ...  
    }  
}

It’s easy to see how silly this code could become if scaled to multiple packages. In order to support both implementations, I just forked the rollbar logging library as well and modified the import to my fork, rather than pkg/errors.

Alternative pkg/errors Link to heading

An alternative implementation would be for pkg/errors to return []uintptr directly and not use a custom type. In this implementation, the rollbar library now doesn’t need to import pkg/errors at all and supports both versions natively.

type stackTracer interface {  
    StackTrace() []uintptr  
}
func logError(err error) {  
    if s, ok := err.(stackTracer); ok {  
        // ...  
    }  
}

This puts no burden on the user of the library to manage dependencies to pkg/errors. The downside is that pkg/errors has now lost type information, or custom functions, on stack trace items.

Why that was not great Link to heading

It’s easy to see how this becomes unscalable in the open source community as multiple people begin to fork a library. It is also impractical in a large company with multiple codebases where teams may not agree on 100% of the shared libraries, or where teams may want to experiment with alternative shared libraries without dragging the company with them all at once. Ideally, the rollbar library would be able to define a package independent stackTrace implementation it expects. This would now support all forks at once.

type StackTrace interface {  
    Stack() []uintptr  
}

type stackTracer interface {  
    StackTrace() StackTrace  
}

func logError(err error) {  
    if s, ok := err.(stackTracer); ok {  
        // ...  
    }  
}

If Go’s type system was flexible enough to allow rollbar to indicate it only required StackTrace().Stack() behavior, it could support multiple forks of the errors package without requiring a dependency.