Skip to content
This repository has been archived by the owner on Dec 1, 2021. It is now read-only.

feat: support std errors functions #213

Merged
merged 13 commits into from
Jan 3, 2020
10 changes: 10 additions & 0 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -286,3 +286,13 @@ func Cause(err error) error {
}
return err
}

func Unwrap(err error) error {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why reimplement this function rather than wrapping it? Someone using Go 1.12 or older will not be using the new style of errors. This creates a risk of incompatibility if Go 1.14 subtly changes its Unwrap implementation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@aperezg emm.... this is a problem if go1.14 changes Unwrap implment, so... we should keep Unwrap >= go1.13?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But why is the problem to maintain the Unwrap function like today? I understand that the Unwrap is an interface on a std library right? Sorry, I try to understand what is the point to change the implementation a wrapped with the standard library. I doubt that they implement a breaking change on that, no?

https://github.com/pkg/errors/blob/master/errors.go#L163

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

go1.14 cannot break the semantics of Unwrap without violating the go1 compat guarantee.

These same considerations apply to successive point releases. For instance, code that runs under Go 1.2 should be compatible with Go 1.2.1, Go 1.3, Go 1.4, etc., although not necessarily with Go 1.1 since it may use features added only in Go 1.2

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This functionality should likely be folded into Cause() and Unwrap() here just calls Cause().

c.f. #215

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as that says, 1.14 errors won't change implementation, so we could make unwrap continue in normal package @aperezg

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That Unwrap only works for specific permutations of wrapping with the standard library and causers.

In the wild, we should expect any possible permutation of wrapping, including multiple times being wrapped in an alternating manner.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unwrap should only unwrap error one times, no matter the inner error implement unwrap interface or not, like std Unwrap do.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But this code would fail to fully unwrap: Unwrap(fmt.Errorf("wrap1: %w", myPrivateCauserOnlyError(fmt.Errorf("wrap 3: %w", err), "wrap2:"))) where I would get the fmt.Errorf("wrap3: %w", err) error not the original err, which would not be the expected behavior.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤦‍♀ the standard library Unwrap does not continuously unwrap a function, until it reaches the end result, unlike how Cause in this package works.

u, ok := err.(interface {
Unwrap() error
})
if !ok {
return nil
}
return u.Unwrap()
}
41 changes: 41 additions & 0 deletions errors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -249,3 +249,44 @@ func TestErrorEquality(t *testing.T) {
}
}
}

func TestUnwrap(t *testing.T) {
err := New("test")

type args struct {
err error
}
tests := []struct {
name string
args args
want error
}{
{
name: "with stack",
args: args{err: WithStack(err)},
want: err,
},
{
name: "with message",
args: args{err: WithMessage(err, "test")},
want: err,
},
{
name: "with message format",
args: args{err: WithMessagef(err, "%s", "test")},
want: err,
},
{
name: "std errors compatibility",
args: args{err: fmt.Errorf("wrap: %w", err)},
want: err,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := Unwrap(tt.args.err); !reflect.DeepEqual(err, tt.want) {
t.Errorf("Unwrap() error = %v, want %v", err, tt.want)
}
})
}
}
31 changes: 31 additions & 0 deletions go113.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// +build go1.13

package errors

import (
stderrors "errors"
)

// Is reports whether any error in err's chain matches target.
//
// The chain consists of err itself followed by the sequence of errors obtained by
// repeatedly calling Unwrap.
//
// An error is considered to match a target if it is equal to that target or if
// it implements a method Is(error) bool such that Is(target) returns true.
func Is(err, target error) bool { return stderrors.Is(err, target) }

// As finds the first error in err's chain that matches target, and if so, sets
// target to that error value and returns true.
//
// The chain consists of err itself followed by the sequence of errors obtained by
// repeatedly calling Unwrap.
//
// An error matches target if the error's concrete value is assignable to the value
// pointed to by target, or if the error has a method As(interface{}) bool such that
// As(target) returns true. In the latter case, the As method is responsible for
// setting target.
//
// As will panic if target is not a non-nil pointer to either a type that implements
// error, or to any interface type. As returns false if err is nil.
func As(err error, target interface{}) bool { return stderrors.As(err, target) }
127 changes: 124 additions & 3 deletions go113_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,135 @@
package errors
Sherlock-Holo marked this conversation as resolved.
Show resolved Hide resolved

import (
stdlib_errors "errors"
stderrors "errors"
"fmt"
"reflect"
"testing"
)

func TestErrorChainCompat(t *testing.T) {
err := stdlib_errors.New("error that gets wrapped")
err := stderrors.New("error that gets wrapped")
wrapped := Wrap(err, "wrapped up")
if !stdlib_errors.Is(wrapped, err) {
if !stderrors.Is(wrapped, err) {
t.Errorf("Wrap does not support Go 1.13 error chains")
}
}

func TestIs(t *testing.T) {
err := New("test")

type args struct {
err error
target error
}
tests := []struct {
name string
args args
want bool
}{
{
name: "with stack",
args: args{
err: WithStack(err),
target: err,
},
want: true,
},
{
name: "with message",
args: args{
err: WithMessage(err, "test"),
target: err,
},
want: true,
},
{
name: "with message format",
args: args{
err: WithMessagef(err, "%s", "test"),
target: err,
},
want: true,
},
{
name: "std errors compatibility",
args: args{
err: fmt.Errorf("wrap it: %w", err),
target: err,
},
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := Is(tt.args.err, tt.args.target); got != tt.want {
t.Errorf("Is() = %v, want %v", got, tt.want)
}
})
}
}

type customErr struct {
msg string
}

func (c customErr) Error() string { return c.msg }

func TestAs(t *testing.T) {
var err = customErr{msg: "test message"}

type args struct {
err error
target interface{}
}
tests := []struct {
name string
args args
want bool
}{
{
name: "with stack",
args: args{
err: WithStack(err),
target: new(customErr),
Sherlock-Holo marked this conversation as resolved.
Show resolved Hide resolved
},
want: true,
},
{
name: "with message",
args: args{
err: WithMessage(err, "test"),
target: new(customErr),
},
want: true,
},
{
name: "with message format",
args: args{
err: WithMessagef(err, "%s", "test"),
target: new(customErr),
},
want: true,
},
{
name: "std errors compatibility",
args: args{
err: fmt.Errorf("wrap it: %w", err),
target: new(customErr),
},
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := As(tt.args.err, tt.args.target); got != tt.want {
t.Errorf("As() = %v, want %v", got, tt.want)
}

ce := tt.args.target.(*customErr)
if !reflect.DeepEqual(err, *ce) {
t.Errorf("set target error failed, target error is %v", *ce)
}
})
}
}