Search code examples
goerror-handlingwrapperopaque-types

How can I wrap a golang error into an opaque error?


How do I wrap an error into an opaque error (as described by Dave Cheney in https://dave.cheney.net/2016/04/27/dont-just-check-errors-handle-them-gracefully)? Also, I want the opaque error to have a stacktrace, and for that to be retained though the return chain.

errors.Wrap() creates a new error with the stacktrace, but not of my opaque type. How do I do both (add the stack trace and make it a MyErr with temporary as true)?

package main

import (
    "fmt"
    "github.com/pkg/errors"
)

type temporary interface {
    Temporary() bool
}

func IsTemporary(err error) bool {
    te, ok := err.(temporary)
    return ok && te.Temporary()
}

type MyError struct {
    error
    isTemporary bool
}

func (e MyError) Temporary() bool {
    return e.isTemporary
}

func f1() error {   // imitate a function from another package, that produces an error
    return fmt.Errorf("f1 error")
}

func f2() error {
    err := f1()
    myErr := errors.Wrap(err, "f2 error")   // Wrap() adds the stacktrace
    // how to wrap it as a temporary MyErr?
    return myErr
}

func f3() error {
    err := f2()
    return fmt.Errorf("f3 error: %+v", err) // don't Wrap() here or we get another stacktrace
}

func f4() error {
    err := f3()
    return fmt.Errorf("f4 error: %+v", err) // the '+' isn't needed here but does no harm
}

func main() {
    err := f4()
    if err != nil {
        if IsTemporary(err) {
            fmt.Println("temporary error")
        }
        fmt.Printf("oops: %+v\n", err)
    }
}

This prints the following:

oops: f4 error: f3 error: f1 error
f2 error
main.f2
        /home/jlearman/projects/axon-internal/ibm/pocs/ibm-cloud/vmware-vms/err2.go:32
main.f3
        /home/jlearman/projects/axon-internal/ibm/pocs/ibm-cloud/vmware-vms/err2.go:38
main.f4
        /home/jlearman/projects/axon-internal/ibm/pocs/ibm-cloud/vmware-vms/err2.go:43
main.main
        /home/jlearman/projects/axon-internal/ibm/pocs/ibm-cloud/vmware-vms/err2.go:48
runtime.main
        /usr/local/go/src/runtime/proc.go:255
runtime.goexit
        /usr/local/go/src/runtime/asm_amd64.s:1581

That's correct except I want to see "temporary error" printed first.

Assume f1 is actually in 3rd party or built-in code, returning a standard error type. f2 is the first function in my code receiving that error, and needs to make it a Temporary when appropriate. (If it's a Temporary originally, that would be a follow-on question but I think I can figure it out.)

I want the pattern for handling errors returned from our code to be consistent throughout the project, which will be relatively large.


Solution

  • You can't really do this with the github.com/pkg/errors function. This is because the error type used for wrapping is unexported, so you can't embed it into your own custom error.

    However seeing as you are not opposed to using an error library other than the stdlib errors package, here is how you could do it with the juju errors package(because it's Err type is exported):

    package main
    
    import (
        "fmt"
    
        "github.com/juju/errors"
    )
    
    type temporary interface {
        Temporary() bool
    }
    
    func IsTemporary(err error) bool {
        for {
            te, ok := err.(temporary)
            if ok {
                return te.Temporary()
            }
    
            er, ok := err.(*errors.Err)
            if ok {
                err = er.Underlying()
                continue
            }
    
            return false
        }
    }
    
    type MyError struct {
        errors.Err
        isTemporary bool
    }
    
    func (e MyError) Temporary() bool {
        return e.isTemporary
    }
    
    func f1() error { // imitate a function from another package, that produces an error
        return errors.Errorf("f1 error")
    }
    
    func f2() error {
        err := f1()
        wrappedErr := errors.Annotate(err, "f2 error")
        return &MyError{
            Err:         *wrappedErr.(*errors.Err),
            isTemporary: true,
        }
    }
    
    func f3() error {
        err := f2()
        return errors.Annotate(err, "f3 error")
    }
    
    func f4() error {
        err := f3()
        return errors.Annotate(err, "f4 error")
    }
    
    func main() {
        err := f4()
        if err != nil {
            if IsTemporary(err) {
                fmt.Println("temporary error")
            }
            if e, ok := err.(*errors.Err); ok {
                fmt.Printf("oops: %+v\n", e.StackTrace())
            }
        }
    }