Skip to content

Commit

Permalink
Merge pull request #35 from hashicorp/f-go113-errors
Browse files Browse the repository at this point in the history
Support Go 1.13 errors.As/Is/Unwrap functionality
  • Loading branch information
mitchellh authored Mar 31, 2020
2 parents ece20dc + 8f55492 commit a5e9814
Show file tree
Hide file tree
Showing 2 changed files with 199 additions and 0 deletions.
67 changes: 67 additions & 0 deletions multierror.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package multierror

import (
"errors"
"fmt"
)

Expand Down Expand Up @@ -49,3 +50,69 @@ func (e *Error) GoString() string {
func (e *Error) WrappedErrors() []error {
return e.Errors
}

// Unwrap returns an error from Error (or nil if there are no errors).
// This error returned will further support Unwrap to get the next error,
// etc. The order will match the order of Errors in the multierror.Error
// at the time of calling.
//
// The resulting error supports errors.As/Is/Unwrap so you can continue
// to use the stdlib errors package to introspect further.
//
// This will perform a shallow copy of the errors slice. Any errors appended
// to this error after calling Unwrap will not be available until a new
// Unwrap is called on the multierror.Error.
func (e *Error) Unwrap() error {
// If we have no errors then we do nothing
if e == nil || len(e.Errors) == 0 {
return nil
}

// If we have exactly one error, we can just return that directly.
if len(e.Errors) == 1 {
return e.Errors[0]
}

// Shallow copy the slice
errs := make([]error, len(e.Errors))
copy(errs, e.Errors)
return chain(errs)
}

// chain implements the interfaces necessary for errors.Is/As/Unwrap to
// work in a deterministic way with multierror. A chain tracks a list of
// errors while accounting for the current represented error. This lets
// Is/As be meaningful.
//
// Unwrap returns the next error. In the cleanest form, Unwrap would return
// the wrapped error here but we can't do that if we want to properly
// get access to all the errors. Instead, users are recommended to use
// Is/As to get the correct error type out.
//
// Precondition: []error is non-empty (len > 0)
type chain []error

// Error implements the error interface
func (e chain) Error() string {
return e[0].Error()
}

// Unwrap implements errors.Unwrap by returning the next error in the
// chain or nil if there are no more errors.
func (e chain) Unwrap() error {
if len(e) == 1 {
return nil
}

return e[1:]
}

// As implements errors.As by attempting to map to the current value.
func (e chain) As(target interface{}) bool {
return errors.As(e[0], target)
}

// Is implements errors.Is by comparing the current value directly.
func (e chain) Is(target error) bool {
return errors.Is(e[0], target)
}
132 changes: 132 additions & 0 deletions multierror_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package multierror

import (
"errors"
"fmt"
"reflect"
"testing"
)
Expand Down Expand Up @@ -69,3 +70,134 @@ func TestErrorWrappedErrors(t *testing.T) {
t.Fatalf("bad: %s", multi.WrappedErrors())
}
}

func TestErrorUnwrap(t *testing.T) {
t.Run("with errors", func(t *testing.T) {
err := &Error{Errors: []error{
errors.New("foo"),
errors.New("bar"),
errors.New("baz"),
}}

var current error = err
for i := 0; i < len(err.Errors); i++ {
current = errors.Unwrap(current)
if !errors.Is(current, err.Errors[i]) {
t.Fatal("should be next value")
}
}

if errors.Unwrap(current) != nil {
t.Fatal("should be nil at the end")
}
})

t.Run("with no errors", func(t *testing.T) {
err := &Error{Errors: nil}
if errors.Unwrap(err) != nil {
t.Fatal("should be nil")
}
})

t.Run("with nil multierror", func(t *testing.T) {
var err *Error
if errors.Unwrap(err) != nil {
t.Fatal("should be nil")
}
})
}

func TestErrorIs(t *testing.T) {
errBar := errors.New("bar")

t.Run("with errBar", func(t *testing.T) {
err := &Error{Errors: []error{
errors.New("foo"),
errBar,
errors.New("baz"),
}}

if !errors.Is(err, errBar) {
t.Fatal("should be true")
}
})

t.Run("with errBar wrapped by fmt.Errorf", func(t *testing.T) {
err := &Error{Errors: []error{
errors.New("foo"),
fmt.Errorf("errorf: %w", errBar),
errors.New("baz"),
}}

if !errors.Is(err, errBar) {
t.Fatal("should be true")
}
})

t.Run("without errBar", func(t *testing.T) {
err := &Error{Errors: []error{
errors.New("foo"),
errors.New("baz"),
}}

if errors.Is(err, errBar) {
t.Fatal("should be false")
}
})
}

func TestErrorAs(t *testing.T) {
match := &nestedError{}

t.Run("with the value", func(t *testing.T) {
err := &Error{Errors: []error{
errors.New("foo"),
match,
errors.New("baz"),
}}

var target *nestedError
if !errors.As(err, &target) {
t.Fatal("should be true")
}
if target == nil {
t.Fatal("target should not be nil")
}
})

t.Run("with the value wrapped by fmt.Errorf", func(t *testing.T) {
err := &Error{Errors: []error{
errors.New("foo"),
fmt.Errorf("errorf: %w", match),
errors.New("baz"),
}}

var target *nestedError
if !errors.As(err, &target) {
t.Fatal("should be true")
}
if target == nil {
t.Fatal("target should not be nil")
}
})

t.Run("without the value", func(t *testing.T) {
err := &Error{Errors: []error{
errors.New("foo"),
errors.New("baz"),
}}

var target *nestedError
if errors.As(err, &target) {
t.Fatal("should be false")
}
if target != nil {
t.Fatal("target should be nil")
}
})
}

// nestedError implements error and is used for tests.
type nestedError struct{}

func (*nestedError) Error() string { return "" }

0 comments on commit a5e9814

Please sign in to comment.