Skip to content

Commit f75ac50

Browse files
authored
Merge pull request #1 from mailgun/thrawn/last
Added errors.Last()
2 parents 8088208 + ce4f2ed commit f75ac50

2 files changed

Lines changed: 83 additions & 0 deletions

File tree

errors.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package errors
22

33
import (
44
"errors"
5+
"reflect"
56
)
67

78
// Import all the standard errors functions as a convenience.
@@ -29,3 +30,54 @@ func New(text string) error {
2930
func Unwrap(err error) error {
3031
return errors.Unwrap(err)
3132
}
33+
34+
// Last finds the last error in err's chain that matches target, and if one is found, sets
35+
// target to that error value and returns true. Otherwise, it returns false.
36+
//
37+
// The chain consists of err itself followed by the sequence of errors obtained by
38+
// repeatedly calling Unwrap.
39+
//
40+
// An error matches target if the error's concrete value is assignable to the value
41+
// pointed to by target, or if the error has a method `As(any) bool` such that
42+
// As(target) returns true.
43+
//
44+
// An error type might provide an As() method so it can be treated as if it were a
45+
// different error type.
46+
//
47+
// Last panics if target is not a non-nil pointer to either a type that implements
48+
// error, or to any interface type.
49+
//
50+
// NOTE: Last() is much slower than As(). Therefore As() should always be used
51+
// unless you absolutely need Last() to retrieve the last error in the error chain
52+
// that matches the target.
53+
func Last(err error, target any) bool {
54+
if target == nil {
55+
panic("errors: target cannot be nil")
56+
}
57+
val := reflect.ValueOf(target)
58+
typ := val.Type()
59+
if typ.Kind() != reflect.Ptr || val.IsNil() {
60+
panic("errors: target must be a non-nil pointer")
61+
}
62+
targetType := typ.Elem()
63+
if targetType.Kind() != reflect.Interface && !targetType.Implements(errorType) {
64+
panic("errors: *target must be interface or implement error")
65+
}
66+
var found error
67+
for err != nil {
68+
if reflect.TypeOf(err).AssignableTo(targetType) {
69+
found = err
70+
}
71+
if x, ok := err.(interface{ As(any) bool }); ok && x.As(target) {
72+
found = err
73+
}
74+
err = Unwrap(err)
75+
}
76+
if found != nil {
77+
val.Elem().Set(reflect.ValueOf(found))
78+
return true
79+
}
80+
return false
81+
}
82+
83+
var errorType = reflect.TypeOf((*error)(nil)).Elem()

errors_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
package errors_test
22

3+
import (
4+
"fmt"
5+
"testing"
6+
7+
"github.com/mailgun/errors"
8+
"github.com/mailgun/errors/callstack"
9+
"github.com/stretchr/testify/assert"
10+
)
11+
312
type ErrTest struct {
413
Msg string
514
}
@@ -30,3 +39,25 @@ func (e *ErrHasFields) Is(target error) bool {
3039
func (e *ErrHasFields) Fields() map[string]interface{} {
3140
return e.F
3241
}
42+
43+
func TestLast(t *testing.T) {
44+
err := errors.New("bottom")
45+
err = errors.Wrap(err, "last")
46+
err = errors.Wrap(err, "second")
47+
err = errors.Wrap(err, "first")
48+
err = fmt.Errorf("wrapped: %w", err)
49+
50+
// errors.As() returns the "first" error in the chain with a stack trace
51+
var first callstack.HasStackTrace
52+
assert.True(t, errors.As(err, &first))
53+
assert.Equal(t, "first: second: last: bottom", first.(error).Error())
54+
55+
// errors.Last() returns the last error in the chain with a stack trace
56+
var last callstack.HasStackTrace
57+
assert.True(t, errors.Last(err, &last))
58+
assert.Equal(t, "last: bottom", last.(error).Error())
59+
60+
// If no stack trace is found, then should not set target and should return false
61+
assert.False(t, errors.Last(errors.New("no stack"), &last))
62+
assert.Equal(t, "last: bottom", last.(error).Error())
63+
}

0 commit comments

Comments
 (0)