Skip to content

Commit 127882e

Browse files
authored
Support errors.As and errors.Is (#28)
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.
1 parent bd075f9 commit 127882e

File tree

4 files changed

+204
-0
lines changed

4 files changed

+204
-0
lines changed

.travis.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ env:
99
go:
1010
- 1.11.x
1111
- 1.12.x
12+
- 1.13.x
1213

1314
cache:
1415
directories:

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
Releases
22
========
33

4+
v1.2.0 (unreleased)
5+
===================
6+
7+
- Support extracting and matching against wrapped errors with `errors.As`
8+
and `errors.Is`.
9+
10+
411
v1.1.0 (2017-06-30)
512
===================
613

go113.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Copyright (c) 2019 Uber Technologies, Inc.
2+
//
3+
// Permission is hereby granted, free of charge, to any person obtaining a copy
4+
// of this software and associated documentation files (the "Software"), to deal
5+
// in the Software without restriction, including without limitation the rights
6+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
// copies of the Software, and to permit persons to whom the Software is
8+
// furnished to do so, subject to the following conditions:
9+
//
10+
// The above copyright notice and this permission notice shall be included in
11+
// all copies or substantial portions of the Software.
12+
//
13+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19+
// THE SOFTWARE.
20+
21+
// +build go1.13
22+
23+
package multierr
24+
25+
import "errors"
26+
27+
// As attempts to find the first error in the error list that matches the type
28+
// of the value that target points to.
29+
//
30+
// This function allows errors.As to traverse the values stored on the
31+
// multierr error.
32+
func (merr *multiError) As(target interface{}) bool {
33+
for _, err := range merr.Errors() {
34+
if errors.As(err, target) {
35+
return true
36+
}
37+
}
38+
return false
39+
}
40+
41+
// Is attempts to match the provided error against errors in the error list.
42+
//
43+
// This function allows errors.Is to traverse the values stored on the
44+
// multierr error.
45+
func (merr *multiError) Is(target error) bool {
46+
for _, err := range merr.Errors() {
47+
if errors.Is(err, target) {
48+
return true
49+
}
50+
}
51+
return false
52+
}

go113_test.go

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
// Copyright (c) 2019 Uber Technologies, Inc.
2+
//
3+
// Permission is hereby granted, free of charge, to any person obtaining a copy
4+
// of this software and associated documentation files (the "Software"), to deal
5+
// in the Software without restriction, including without limitation the rights
6+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
// copies of the Software, and to permit persons to whom the Software is
8+
// furnished to do so, subject to the following conditions:
9+
//
10+
// The above copyright notice and this permission notice shall be included in
11+
// all copies or substantial portions of the Software.
12+
//
13+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19+
// THE SOFTWARE.
20+
21+
// +build go1.13
22+
23+
package multierr_test
24+
25+
import (
26+
"errors"
27+
"os"
28+
"testing"
29+
30+
"github.com/stretchr/testify/assert"
31+
"github.com/stretchr/testify/require"
32+
"go.uber.org/multierr"
33+
)
34+
35+
type errGreatSadness struct{ id int }
36+
37+
func (errGreatSadness) Error() string {
38+
return "great sadness"
39+
}
40+
41+
type errUnprecedentedFailure struct{ id int }
42+
43+
func (errUnprecedentedFailure) Error() string {
44+
return "unprecedented failure"
45+
}
46+
47+
func (e errUnprecedentedFailure) Unwrap() error {
48+
return errRootCause{e.id}
49+
}
50+
51+
type errRootCause struct{ i int }
52+
53+
func (errRootCause) Error() string {
54+
return "root cause"
55+
}
56+
57+
func TestErrorsWrapping(t *testing.T) {
58+
err := multierr.Append(
59+
errGreatSadness{42},
60+
errUnprecedentedFailure{43},
61+
)
62+
63+
t.Run("left", func(t *testing.T) {
64+
t.Run("As", func(t *testing.T) {
65+
var got errGreatSadness
66+
require.True(t, errors.As(err, &got))
67+
assert.Equal(t, 42, got.id)
68+
})
69+
70+
t.Run("Is", func(t *testing.T) {
71+
assert.False(t, errors.Is(err, errGreatSadness{41}))
72+
assert.True(t, errors.Is(err, errGreatSadness{42}))
73+
})
74+
})
75+
76+
t.Run("right", func(t *testing.T) {
77+
t.Run("As", func(t *testing.T) {
78+
var got errUnprecedentedFailure
79+
require.True(t, errors.As(err, &got))
80+
assert.Equal(t, 43, got.id)
81+
})
82+
83+
t.Run("Is", func(t *testing.T) {
84+
assert.False(t, errors.Is(err, errUnprecedentedFailure{42}))
85+
assert.True(t, errors.Is(err, errUnprecedentedFailure{43}))
86+
})
87+
})
88+
89+
t.Run("top-level", func(t *testing.T) {
90+
t.Run("As", func(t *testing.T) {
91+
var got interface{ Errors() []error }
92+
require.True(t, errors.As(err, &got))
93+
assert.Len(t, got.Errors(), 2)
94+
})
95+
96+
t.Run("Is", func(t *testing.T) {
97+
assert.True(t, errors.Is(err, err))
98+
})
99+
})
100+
101+
t.Run("root cause", func(t *testing.T) {
102+
t.Run("As", func(t *testing.T) {
103+
var got errRootCause
104+
require.True(t, errors.As(err, &got))
105+
assert.Equal(t, 43, got.i)
106+
})
107+
108+
t.Run("Is", func(t *testing.T) {
109+
assert.False(t, errors.Is(err, errRootCause{42}))
110+
assert.True(t, errors.Is(err, errRootCause{43}))
111+
})
112+
})
113+
114+
t.Run("mismatch", func(t *testing.T) {
115+
t.Run("As", func(t *testing.T) {
116+
var got *os.PathError
117+
assert.False(t, errors.As(err, &got))
118+
})
119+
120+
t.Run("Is", func(t *testing.T) {
121+
assert.False(t, errors.Is(err, errors.New("great sadness")))
122+
})
123+
})
124+
}
125+
126+
func TestErrorsWrappingSameType(t *testing.T) {
127+
err := multierr.Combine(
128+
errGreatSadness{1},
129+
errGreatSadness{2},
130+
errGreatSadness{3},
131+
)
132+
133+
t.Run("As returns first", func(t *testing.T) {
134+
var got errGreatSadness
135+
require.True(t, errors.As(err, &got))
136+
assert.Equal(t, 1, got.id)
137+
})
138+
139+
t.Run("Is matches all", func(t *testing.T) {
140+
assert.True(t, errors.Is(err, errGreatSadness{1}))
141+
assert.True(t, errors.Is(err, errGreatSadness{2}))
142+
assert.True(t, errors.Is(err, errGreatSadness{3}))
143+
})
144+
}

0 commit comments

Comments
 (0)