Skip to content

Commit

Permalink
Support errors.As and errors.Is (#28)
Browse files Browse the repository at this point in the history
Go 1.13 includes the `errors.As` and `errors.Is` APIs.

The base functionality provided by these functions is to cast errors in
an error chain to a specific type or check for equality.

The only definition of error chain currently supported is via errors
which return the underlying cause with a `Unwrap() error` method, but
the design does not make any assumptions otherwise.

Specifically, both, `errors.As` and `errors.Is` support customizing
their behavior by implementing,

    interface { As(interface{}) bool }
    interface { Is(error) bool }

This change does exactly that, making it possible to extract or match
against individual errors in a multierr error. This will work for both,
top-level errors in a multierr error, as well as for errors wrapped by
any of those errors.
  • Loading branch information
abhinav authored Sep 26, 2019
1 parent bd075f9 commit 127882e
Show file tree
Hide file tree
Showing 4 changed files with 204 additions and 0 deletions.
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ env:
go:
- 1.11.x
- 1.12.x
- 1.13.x

cache:
directories:
Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
Releases
========

v1.2.0 (unreleased)
===================

- Support extracting and matching against wrapped errors with `errors.As`
and `errors.Is`.


v1.1.0 (2017-06-30)
===================

Expand Down
52 changes: 52 additions & 0 deletions go113.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Copyright (c) 2019 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

// +build go1.13

package multierr

import "errors"

// As attempts to find the first error in the error list that matches the type
// of the value that target points to.
//
// This function allows errors.As to traverse the values stored on the
// multierr error.
func (merr *multiError) As(target interface{}) bool {
for _, err := range merr.Errors() {
if errors.As(err, target) {
return true
}
}
return false
}

// Is attempts to match the provided error against errors in the error list.
//
// This function allows errors.Is to traverse the values stored on the
// multierr error.
func (merr *multiError) Is(target error) bool {
for _, err := range merr.Errors() {
if errors.Is(err, target) {
return true
}
}
return false
}
144 changes: 144 additions & 0 deletions go113_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
// Copyright (c) 2019 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

// +build go1.13

package multierr_test

import (
"errors"
"os"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/multierr"
)

type errGreatSadness struct{ id int }

func (errGreatSadness) Error() string {
return "great sadness"
}

type errUnprecedentedFailure struct{ id int }

func (errUnprecedentedFailure) Error() string {
return "unprecedented failure"
}

func (e errUnprecedentedFailure) Unwrap() error {
return errRootCause{e.id}
}

type errRootCause struct{ i int }

func (errRootCause) Error() string {
return "root cause"
}

func TestErrorsWrapping(t *testing.T) {
err := multierr.Append(
errGreatSadness{42},
errUnprecedentedFailure{43},
)

t.Run("left", func(t *testing.T) {
t.Run("As", func(t *testing.T) {
var got errGreatSadness
require.True(t, errors.As(err, &got))
assert.Equal(t, 42, got.id)
})

t.Run("Is", func(t *testing.T) {
assert.False(t, errors.Is(err, errGreatSadness{41}))
assert.True(t, errors.Is(err, errGreatSadness{42}))
})
})

t.Run("right", func(t *testing.T) {
t.Run("As", func(t *testing.T) {
var got errUnprecedentedFailure
require.True(t, errors.As(err, &got))
assert.Equal(t, 43, got.id)
})

t.Run("Is", func(t *testing.T) {
assert.False(t, errors.Is(err, errUnprecedentedFailure{42}))
assert.True(t, errors.Is(err, errUnprecedentedFailure{43}))
})
})

t.Run("top-level", func(t *testing.T) {
t.Run("As", func(t *testing.T) {
var got interface{ Errors() []error }
require.True(t, errors.As(err, &got))
assert.Len(t, got.Errors(), 2)
})

t.Run("Is", func(t *testing.T) {
assert.True(t, errors.Is(err, err))
})
})

t.Run("root cause", func(t *testing.T) {
t.Run("As", func(t *testing.T) {
var got errRootCause
require.True(t, errors.As(err, &got))
assert.Equal(t, 43, got.i)
})

t.Run("Is", func(t *testing.T) {
assert.False(t, errors.Is(err, errRootCause{42}))
assert.True(t, errors.Is(err, errRootCause{43}))
})
})

t.Run("mismatch", func(t *testing.T) {
t.Run("As", func(t *testing.T) {
var got *os.PathError
assert.False(t, errors.As(err, &got))
})

t.Run("Is", func(t *testing.T) {
assert.False(t, errors.Is(err, errors.New("great sadness")))
})
})
}

func TestErrorsWrappingSameType(t *testing.T) {
err := multierr.Combine(
errGreatSadness{1},
errGreatSadness{2},
errGreatSadness{3},
)

t.Run("As returns first", func(t *testing.T) {
var got errGreatSadness
require.True(t, errors.As(err, &got))
assert.Equal(t, 1, got.id)
})

t.Run("Is matches all", func(t *testing.T) {
assert.True(t, errors.Is(err, errGreatSadness{1}))
assert.True(t, errors.Is(err, errGreatSadness{2}))
assert.True(t, errors.Is(err, errGreatSadness{3}))
})
}

0 comments on commit 127882e

Please sign in to comment.