pkg / errors

Simple error handling primitives

Home Page:https://godoc.org/github.com/pkg/errors

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Questions about extending annotations

pantsmann opened this issue · comments

I've been experimenting with asserting errors for behavior. I've found this gets more complicated if the behavior is not added at the error origin. Suppose we have the following:

func IsSpecial(err error) bool {
    type special interface{
        Special() bool
    }
    var s special
    ok := errors.As(err, &s)
    return ok && s.Special()
}
func IsExtraSpecial(err error) bool {
    type xspecial interface{
        XSpecial() bool
     }
     var x xspecial
     ok := errors.As(err, &x)
     return ok && x.XSpecial()
}

We also have a long sequence of function calls that wrap errors before returning them. Somewhere in this chain of calls we have the following:

type spcl struct{ error }
func (s spcl) Special() bool { return true }

We also have this in another location of the chain:

type xspcl struct{ error }
func (x xspcl) XSpecial() bool { return true }

Various functions in the call chain could add special and extra special behaviors to errors like this:

return spcl{ errors.Wrap(err, "interesting failure")}

At the root of this call chain, we might want to test for these behaviors with the IsSpecial and IsExtraSpecial functions, but we'd find there were problems. First, since neither spcl or xspcl implement the Wrapper interface the tests might not work. Second, if we tried to print out the stack traces all along the error chain we wouldn't be able to since spcl and xspcl don't implement fmt.Formatter. We can add these functions of course but it seems silly since the errors we are using to instantiate spcl and xspcl already implement the proper functions. I could change things as follows:

type extendedError interface {
    error
    fmt.Formatter
    errors.Wrapper
}
type spcl struct { extendedError }

Now adding the special behavior to an error would look like this:

return spcl{ errors.Wrap(err, "interesting failure").(extendedError)}

Great, this works, but I ask myself why not have pkg/errors define ExtendedError once and have Wrap return this type instead of error. This would make adding these kinds of special behaviors really easy and cheap. The IsSpecial and IsExtraSpecial functions would stay the same. Everything else would be as follows:

type spcl struct ( errors.ExtendedError }
func (s spcl) Special() bool { return true }
...
return spcl(errors.Wrap(err, "interesting error")}

What issues/problems might there be with doing this? Idiomatic problems? etc?

If I also tweaked the errors.foundational struct to implement Wrapper but always return nil (and tweaked Cause() to recognize this) I could add behaviors to the original errors with equal ease.

Don’t use:

type extendedError interface {
    error
    fmt.Formatter
    errors.Wrapper // this interface intentionally does not exist
}
type spcl struct { extendedError }

return spcl{ io.EOF } // this will not work

We are unlikely to ever adopt returning anything other than a simple error. A central design principle of Go is that errors should try to always be returned with the error interface type, regardless of what the underlying implementation or method set implementation is.

You are struggling here because of the strict typing in Go. This strictness allows strong assertions to be made at compile-time about what you’re trying to do to what. But this means that when you embed a type into a struct, that struct (because it is a concrete type) has to have a known-at-compile-time unchanging implementation set. This means that when embedding an interface, the struct can only guarantee the specific methods implemented by the interface.

If you want to be able to pass on Format etc, then you need to properly expand the type, and not just embed the error type directly into the type.

The solution here is to properly implement a wrapping error type, there are no shortcuts:

type spcl struct{ wrap error }
func (s spcl) Error() string { return s.wrap.Error() }
func (s spcl) Unwrap() error { return s.wrap }
func (s spcl) Format(f State, verb rune) {
	if formatter, ok := s.wrap.(fmt.Formatter); ok {
		return formatter.Format(f, verb)
	}

	switch verb {
	case 'v':
		if f.Flag('+') {
			fmt.Fprintf(f, "%+v", s.wrap)
			return
		}
		fallthrough
	case 's':
		io.WriteString(f, s.wrap)
	case 'q':
		fmt.Fprintf(f, "%q", s.wrap)
	}
}
func (s spcl) Special() bool { return true }

I guess what I'm saying is that there IS a short cut but I'm unsure why I shouldn't use it.
So I guess two questions. (Note I'm using the github.com/friendsofgo/errors fork of github.com/pkg/errors, but they are very similar)

First, why not do one of the following, all of which appear to work correctly and represent what I was trying to explain before more completely. (all this code was originally separated out into different packages roughly where '///////'s are. See spcl1, spcl2, and spcl3, the point being not rewriting the stuff in BaseError over and over again) :

type ExtError interface {
	error
	fmt.Formatter
	errors.Wrapper
}

type BaseError struct{ Err error }

func (b BaseError) Error() string { return b.Err.Error() }
func (b BaseError) Unwrap() error { return b.Err }
func (b BaseError) Format(f fmt.State, verb rune) {
	switch verb {
	case 'v':
		if f.Flag('+') {
			fmt.Fprintf(f, "%+v", b.Err)
			return
		}
		fallthrough
	case 's':
		io.WriteString(f, b.Err.Error())
	case 'q':
		fmt.Fprintf(f, "%q", b.Err)
	}
}

/////////////////////////////////////////////////////////

type spcl1 struct {
	ExtError
}

func (s spcl1)Special1() bool {
	return true
}

func GetError() error {
	return spcl1{errors.Wrap(GetError2(), "I'm special").(ExtError)}
}

//////////////////////////////////////////////////

type spcl2 struct {
	ExtError
}

func (s spcl2) Special2() bool {
	return true
}

func GetError2() error {
	err := errors.Wrap(GetError3(), "I'm special too")
	ee, ok := err.(ExtError)
	if ok {
		return spcl2{ee}
	}
	return spcl2{BaseError{err}}
}

///////////////////////////////////////////

type spcl3 struct {
	BaseError
}

func (s spcl3) Special3() bool {
	return true
}

func GetError3() error {
	err := errors.New("I'm special three?")
	return spcl3{BaseError{err}}
}

////////////////////////////////////////////////////

func IsSpecial3(err error) bool {
	type spcl3 interface {
		error
		Special3() bool
	}

	var s spcl3
	ok := errors.As(err, &s)
	return ok && s.Special3()
}

func IsSpecial2(err error) bool {
	type spcl2 interface {
		error
		Special2() bool
	}

	var s spcl2
	ok := errors.As(err, &s)
	return ok && s.Special2()
}

func IsSpecial1(err error) bool {
	type spcl1 interface {
		error
		Special1() bool
	}

	var s spcl1
	ok := errors.As(err, &s)
	return ok && s.Special1()
}

func main() {
	err := errors.Wrap(GetError(), "root")
	fmt.Printf("is one: %v\n", IsSpecial1(err))
	fmt.Printf("is two: %v\n", IsSpecial2(err))
	fmt.Printf("is three: %v\n", IsSpecial3(err))
	fmt.Printf("%v\n", err)
	fmt.Printf("%+v\n", err)
}

Second, I'm aware that idiomatic Go would return 'error' and not some other type of error. In this case, however, I see a bit of an advantage in having errors.Wrap(), errors.WithStack(), and errors.WithMessage() returning a type that correlates to ExtError from the example above. That return type still fulfills 'error' and could be used wherever an 'error' could be used. It would save me from needing to define ExtError myself and save me from some type assertions. The bigger advantage is that it defines what is necessary for Wrap, WithStack, and WithMessage to work properly from the top of an error chain to the bottom, so somebody like me doesn't come along and break everything by adding special behaviors that don't meet pkg/errors' semantics. So my second question is why does this not trump the Go suggestion/requirement of always returning the 'error' type instead of a more specific, expansive error type?

If anyone is curious here is the output of running my example code (when it was still separated into multiple files in various packages).

is one: true
is two: true
is three: true
root: I'm special: I'm special too: I'm special three?
I'm special three?
.../temp/temp/temp.GetError
        /c/gocode/src/.../temp/temp/temp/temp.go:17
.../temp/temp.GetError
        /c/gocode/src/.../temp/temp/temp.go:18
main.GetError
        /c/gocode/src/.../temp/temp.go:20
main.main
        /c/gocode/src/.../temp/temp.go:57
runtime.main
        /usr/local/go/src/runtime/proc.go:201
runtime.goexit
        /usr/local/go/src/runtime/asm_amd64.s:1333
I'm special too
.../temp/temp.GetError
        /c/gocode/src/.../temp/temp/temp.go:18
main.GetError
        /c/gocode/src/.../temp/temp.go:20
main.main
        /c/gocode/src/.../temp/temp.go:57
runtime.main
        /usr/local/go/src/runtime/proc.go:201
runtime.goexit
        /usr/local/go/src/runtime/asm_amd64.s:1333
I'm special
main.GetError
        /c/gocode/src/.../temp/temp.go:20
main.main
        /c/gocode/src/.../temp/temp.go:57
runtime.main
        /usr/local/go/src/runtime/proc.go:201
runtime.goexit
        /usr/local/go/src/runtime/asm_amd64.s:1333
root
main.main
        /c/gocode/src/.../temp/temp.go:57
runtime.main
        /usr/local/go/src/runtime/proc.go:201
runtime.goexit
        /usr/local/go/src/runtime/asm_amd64.s:1333

With your recommended “return a wider interface”:

err := errors.Wrap(io.EOF, "foo")
…
err = json.Unmarshal(data, &var) // compilation error: error does not implement ExtendedError

Errors should always be returned as errors, because even changing the return type to an expanded error type can break compatibility of types in existing code.

Also, you are still not going to know when a downstream call is narrowing the interface on you, so you’re still always going to need to do the type assertion anyways:

func myLibraryFunc() error {
	return errors.Wrap(doThing(), "bar")
}

If you know that your types will always be wrapping something that implements your extended interface, then go ahead and embed the extended interface in your own types locally, and type assert the return from errors.With* and 'errors.Wrap*`, that’s what type assertion is there for.

But returning that extended type from pkg/errors itself is not something that is going to happen, because doing so can unreasonably break other people’s code. Note also, nothing stops you from forking the repo, and have it return the extended interface. But a PR to do this would not be entertained, as it would break too much code.

That makes sense. It is still a bit tempting to do this in my own code, but I think I won't. It makes sense to me to use the type assertions. As you say, that's what they are for. The example at the top of your comment really clears up my second question. Danke.