Skip to content

Commit 4a0a2b3

Browse files
committed
errors, fmt: add support for wrapping multiple errors
An error which implements an "Unwrap() []error" method wraps all the non-nil errors in the returned []error. We replace the concept of the "error chain" inspected by errors.Is and errors.As with the "error tree". Is and As perform a pre-order, depth-first traversal of an error's tree. As returns the first matching result, if any. The new errors.Join function returns an error wrapping a list of errors. The fmt.Errorf function now supports multiple instances of the %w verb. For golang#53435. Change-Id: Ib7402e70b68e28af8f201d2b66bd8e87ccfb5283 Reviewed-on: https://go-review.googlesource.com/c/go/+/432898 Reviewed-by: Cherry Mui <cherryyz@google.com> Reviewed-by: Rob Pike <r@golang.org> Run-TryBot: Damien Neil <dneil@google.com> Reviewed-by: Joseph Tsai <joetsai@digital-static.net>
1 parent 36f046d commit 4a0a2b3

File tree

10 files changed

+325
-51
lines changed

10 files changed

+325
-51
lines changed

api/next/53435.txt

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pkg errors, func Join(...error) error #53435

src/errors/errors.go

+17-14
Original file line numberDiff line numberDiff line change
@@ -6,26 +6,29 @@
66
//
77
// The New function creates errors whose only content is a text message.
88
//
9-
// The Unwrap, Is and As functions work on errors that may wrap other errors.
10-
// An error wraps another error if its type has the method
9+
// An error e wraps another error if e's type has one of the methods
1110
//
1211
// Unwrap() error
12+
// Unwrap() []error
1313
//
14-
// If e.Unwrap() returns a non-nil error w, then we say that e wraps w.
14+
// If e.Unwrap() returns a non-nil error w or a slice containing w,
15+
// then we say that e wraps w. A nil error returned from e.Unwrap()
16+
// indicates that e does not wrap any error. It is invalid for an
17+
// Unwrap method to return an []error containing a nil error value.
1518
//
16-
// Unwrap unpacks wrapped errors. If its argument's type has an
17-
// Unwrap method, it calls the method once. Otherwise, it returns nil.
19+
// An easy way to create wrapped errors is to call fmt.Errorf and apply
20+
// the %w verb to the error argument:
1821
//
19-
// A simple way to create wrapped errors is to call fmt.Errorf and apply the %w verb
20-
// to the error argument:
22+
// wrapsErr := fmt.Errorf("... %w ...", ..., err, ...)
2123
//
22-
// errors.Unwrap(fmt.Errorf("... %w ...", ..., err, ...))
24+
// Successive unwrapping of an error creates a tree. The Is and As
25+
// functions inspect an error's tree by examining first the error
26+
// itself followed by the tree of each of its children in turn
27+
// (pre-order, depth-first traversal).
2328
//
24-
// returns err.
25-
//
26-
// Is unwraps its first argument sequentially looking for an error that matches the
27-
// second. It reports whether it finds a match. It should be used in preference to
28-
// simple equality checks:
29+
// Is examines the tree of its first argument looking for an error that
30+
// matches the second. It reports whether it finds a match. It should be
31+
// used in preference to simple equality checks:
2932
//
3033
// if errors.Is(err, fs.ErrExist)
3134
//
@@ -35,7 +38,7 @@
3538
//
3639
// because the former will succeed if err wraps fs.ErrExist.
3740
//
38-
// As unwraps its first argument sequentially looking for an error that can be
41+
// As examines the tree of its first argument looking for an error that can be
3942
// assigned to its second argument, which must be a pointer. If it succeeds, it
4043
// performs the assignment and returns true. Otherwise, it returns false. The form
4144
//

src/errors/errors_test.go

+18
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,21 @@ func ExampleNew_errorf() {
5151
}
5252
// Output: user "bimmler" (id 17) not found
5353
}
54+
55+
func ExampleJoin() {
56+
err1 := errors.New("err1")
57+
err2 := errors.New("err2")
58+
err := errors.Join(err1, err2)
59+
fmt.Println(err)
60+
if errors.Is(err, err1) {
61+
fmt.Println("err is err1")
62+
}
63+
if errors.Is(err, err2) {
64+
fmt.Println("err is err2")
65+
}
66+
// Output:
67+
// err1
68+
// err2
69+
// err is err1
70+
// err is err2
71+
}

src/errors/join.go

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Copyright 2022 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package errors
6+
7+
// Join returns an error that wraps the given errors.
8+
// Any nil error values are discarded.
9+
// Join returns nil if errs contains no non-nil values.
10+
// The error formats as the concatenation of the strings obtained
11+
// by calling the Error method of each element of errs, with a newline
12+
// between each string.
13+
func Join(errs ...error) error {
14+
n := 0
15+
for _, err := range errs {
16+
if err != nil {
17+
n++
18+
}
19+
}
20+
if n == 0 {
21+
return nil
22+
}
23+
e := &joinError{
24+
errs: make([]error, 0, n),
25+
}
26+
for _, err := range errs {
27+
if err != nil {
28+
e.errs = append(e.errs, err)
29+
}
30+
}
31+
return e
32+
}
33+
34+
type joinError struct {
35+
errs []error
36+
}
37+
38+
func (e *joinError) Error() string {
39+
var b []byte
40+
for i, err := range e.errs {
41+
if i > 0 {
42+
b = append(b, '\n')
43+
}
44+
b = append(b, err.Error()...)
45+
}
46+
return string(b)
47+
}
48+
49+
func (e *joinError) Unwrap() []error {
50+
return e.errs
51+
}

src/errors/join_test.go

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// Copyright 2022 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package errors_test
6+
7+
import (
8+
"errors"
9+
"reflect"
10+
"testing"
11+
)
12+
13+
func TestJoinReturnsNil(t *testing.T) {
14+
if err := errors.Join(); err != nil {
15+
t.Errorf("errors.Join() = %v, want nil", err)
16+
}
17+
if err := errors.Join(nil); err != nil {
18+
t.Errorf("errors.Join(nil) = %v, want nil", err)
19+
}
20+
if err := errors.Join(nil, nil); err != nil {
21+
t.Errorf("errors.Join(nil, nil) = %v, want nil", err)
22+
}
23+
}
24+
25+
func TestJoin(t *testing.T) {
26+
err1 := errors.New("err1")
27+
err2 := errors.New("err2")
28+
for _, test := range []struct {
29+
errs []error
30+
want []error
31+
}{{
32+
errs: []error{err1},
33+
want: []error{err1},
34+
}, {
35+
errs: []error{err1, err2},
36+
want: []error{err1, err2},
37+
}, {
38+
errs: []error{err1, nil, err2},
39+
want: []error{err1, err2},
40+
}} {
41+
got := errors.Join(test.errs...).(interface{ Unwrap() []error }).Unwrap()
42+
if !reflect.DeepEqual(got, test.want) {
43+
t.Errorf("Join(%v) = %v; want %v", test.errs, got, test.want)
44+
}
45+
if len(got) != cap(got) {
46+
t.Errorf("Join(%v) returns errors with len=%v, cap=%v; want len==cap", test.errs, len(got), cap(got))
47+
}
48+
}
49+
}

src/errors/wrap.go

+44-13
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import (
1111
// Unwrap returns the result of calling the Unwrap method on err, if err's
1212
// type contains an Unwrap method returning error.
1313
// Otherwise, Unwrap returns nil.
14+
//
15+
// Unwrap returns nil if the Unwrap method returns []error.
1416
func Unwrap(err error) error {
1517
u, ok := err.(interface {
1618
Unwrap() error
@@ -21,10 +23,11 @@ func Unwrap(err error) error {
2123
return u.Unwrap()
2224
}
2325

24-
// Is reports whether any error in err's chain matches target.
26+
// Is reports whether any error in err's tree matches target.
2527
//
26-
// The chain consists of err itself followed by the sequence of errors obtained by
27-
// repeatedly calling Unwrap.
28+
// The tree consists of err itself, followed by the errors obtained by repeatedly
29+
// calling Unwrap. When err wraps multiple errors, Is examines err followed by a
30+
// depth-first traversal of its children.
2831
//
2932
// An error is considered to match a target if it is equal to that target or if
3033
// it implements a method Is(error) bool such that Is(target) returns true.
@@ -50,20 +53,31 @@ func Is(err, target error) bool {
5053
if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {
5154
return true
5255
}
53-
// TODO: consider supporting target.Is(err). This would allow
54-
// user-definable predicates, but also may allow for coping with sloppy
55-
// APIs, thereby making it easier to get away with them.
56-
if err = Unwrap(err); err == nil {
56+
switch x := err.(type) {
57+
case interface{ Unwrap() error }:
58+
err = x.Unwrap()
59+
if err == nil {
60+
return false
61+
}
62+
case interface{ Unwrap() []error }:
63+
for _, err := range x.Unwrap() {
64+
if Is(err, target) {
65+
return true
66+
}
67+
}
68+
return false
69+
default:
5770
return false
5871
}
5972
}
6073
}
6174

62-
// As finds the first error in err's chain that matches target, and if one is found, sets
75+
// As finds the first error in err's tree that matches target, and if one is found, sets
6376
// target to that error value and returns true. Otherwise, it returns false.
6477
//
65-
// The chain consists of err itself followed by the sequence of errors obtained by
66-
// repeatedly calling Unwrap.
78+
// The tree consists of err itself, followed by the errors obtained by repeatedly
79+
// calling Unwrap. When err wraps multiple errors, As examines err followed by a
80+
// depth-first traversal of its children.
6781
//
6882
// An error matches target if the error's concrete value is assignable to the value
6983
// pointed to by target, or if the error has a method As(interface{}) bool such that
@@ -76,6 +90,9 @@ func Is(err, target error) bool {
7690
// As panics if target is not a non-nil pointer to either a type that implements
7791
// error, or to any interface type.
7892
func As(err error, target any) bool {
93+
if err == nil {
94+
return false
95+
}
7996
if target == nil {
8097
panic("errors: target cannot be nil")
8198
}
@@ -88,17 +105,31 @@ func As(err error, target any) bool {
88105
if targetType.Kind() != reflectlite.Interface && !targetType.Implements(errorType) {
89106
panic("errors: *target must be interface or implement error")
90107
}
91-
for err != nil {
108+
for {
92109
if reflectlite.TypeOf(err).AssignableTo(targetType) {
93110
val.Elem().Set(reflectlite.ValueOf(err))
94111
return true
95112
}
96113
if x, ok := err.(interface{ As(any) bool }); ok && x.As(target) {
97114
return true
98115
}
99-
err = Unwrap(err)
116+
switch x := err.(type) {
117+
case interface{ Unwrap() error }:
118+
err = x.Unwrap()
119+
if err == nil {
120+
return false
121+
}
122+
case interface{ Unwrap() []error }:
123+
for _, err := range x.Unwrap() {
124+
if As(err, target) {
125+
return true
126+
}
127+
}
128+
return false
129+
default:
130+
return false
131+
}
100132
}
101-
return false
102133
}
103134

104135
var errorType = reflectlite.TypeOf((*error)(nil)).Elem()

src/errors/wrap_test.go

+51-1
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,17 @@ func TestIs(t *testing.T) {
4747
{&errorUncomparable{}, &errorUncomparable{}, false},
4848
{errorUncomparable{}, err1, false},
4949
{&errorUncomparable{}, err1, false},
50+
{multiErr{}, err1, false},
51+
{multiErr{err1, err3}, err1, true},
52+
{multiErr{err3, err1}, err1, true},
53+
{multiErr{err1, err3}, errors.New("x"), false},
54+
{multiErr{err3, errb}, errb, true},
55+
{multiErr{err3, errb}, erra, true},
56+
{multiErr{err3, errb}, err1, true},
57+
{multiErr{errb, err3}, err1, true},
58+
{multiErr{poser}, err1, true},
59+
{multiErr{poser}, err3, true},
60+
{multiErr{nil}, nil, false},
5061
}
5162
for _, tc := range testCases {
5263
t.Run("", func(t *testing.T) {
@@ -148,6 +159,41 @@ func TestAs(t *testing.T) {
148159
&timeout,
149160
true,
150161
errF,
162+
}, {
163+
multiErr{},
164+
&errT,
165+
false,
166+
nil,
167+
}, {
168+
multiErr{errors.New("a"), errorT{"T"}},
169+
&errT,
170+
true,
171+
errorT{"T"},
172+
}, {
173+
multiErr{errorT{"T"}, errors.New("a")},
174+
&errT,
175+
true,
176+
errorT{"T"},
177+
}, {
178+
multiErr{errorT{"a"}, errorT{"b"}},
179+
&errT,
180+
true,
181+
errorT{"a"},
182+
}, {
183+
multiErr{multiErr{errors.New("a"), errorT{"a"}}, errorT{"b"}},
184+
&errT,
185+
true,
186+
errorT{"a"},
187+
}, {
188+
multiErr{wrapped{"path error", errF}},
189+
&timeout,
190+
true,
191+
errF,
192+
}, {
193+
multiErr{nil},
194+
&errT,
195+
false,
196+
nil,
151197
}}
152198
for i, tc := range testCases {
153199
name := fmt.Sprintf("%d:As(Errorf(..., %v), %v)", i, tc.err, tc.target)
@@ -223,9 +269,13 @@ type wrapped struct {
223269
}
224270

225271
func (e wrapped) Error() string { return e.msg }
226-
227272
func (e wrapped) Unwrap() error { return e.err }
228273

274+
type multiErr []error
275+
276+
func (m multiErr) Error() string { return "multiError" }
277+
func (m multiErr) Unwrap() []error { return []error(m) }
278+
229279
type errorUncomparable struct {
230280
f []string
231281
}

0 commit comments

Comments
 (0)