Skip to content

Commit

Permalink
errors, fmt: add support for wrapping multiple errors
Browse files Browse the repository at this point in the history
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 <[email protected]>
Reviewed-by: Rob Pike <[email protected]>
Run-TryBot: Damien Neil <[email protected]>
Reviewed-by: Joseph Tsai <[email protected]>
  • Loading branch information
neild committed Sep 29, 2022
1 parent 36f046d commit 4a0a2b3
Show file tree
Hide file tree
Showing 10 changed files with 325 additions and 51 deletions.
1 change: 1 addition & 0 deletions api/next/53435.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pkg errors, func Join(...error) error #53435
31 changes: 17 additions & 14 deletions src/errors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,29 @@
//
// The New function creates errors whose only content is a text message.
//
// The Unwrap, Is and As functions work on errors that may wrap other errors.
// An error wraps another error if its type has the method
// An error e wraps another error if e's type has one of the methods
//
// Unwrap() error
// Unwrap() []error
//
// If e.Unwrap() returns a non-nil error w, then we say that e wraps w.
// If e.Unwrap() returns a non-nil error w or a slice containing w,
// then we say that e wraps w. A nil error returned from e.Unwrap()
// indicates that e does not wrap any error. It is invalid for an
// Unwrap method to return an []error containing a nil error value.
//
// Unwrap unpacks wrapped errors. If its argument's type has an
// Unwrap method, it calls the method once. Otherwise, it returns nil.
// An easy way to create wrapped errors is to call fmt.Errorf and apply
// the %w verb to the error argument:
//
// A simple way to create wrapped errors is to call fmt.Errorf and apply the %w verb
// to the error argument:
// wrapsErr := fmt.Errorf("... %w ...", ..., err, ...)
//
// errors.Unwrap(fmt.Errorf("... %w ...", ..., err, ...))
// Successive unwrapping of an error creates a tree. The Is and As
// functions inspect an error's tree by examining first the error
// itself followed by the tree of each of its children in turn
// (pre-order, depth-first traversal).
//
// returns err.
//
// Is unwraps its first argument sequentially looking for an error that matches the
// second. It reports whether it finds a match. It should be used in preference to
// simple equality checks:
// Is examines the tree of its first argument looking for an error that
// matches the second. It reports whether it finds a match. It should be
// used in preference to simple equality checks:
//
// if errors.Is(err, fs.ErrExist)
//
Expand All @@ -35,7 +38,7 @@
//
// because the former will succeed if err wraps fs.ErrExist.
//
// As unwraps its first argument sequentially looking for an error that can be
// As examines the tree of its first argument looking for an error that can be
// assigned to its second argument, which must be a pointer. If it succeeds, it
// performs the assignment and returns true. Otherwise, it returns false. The form
//
Expand Down
18 changes: 18 additions & 0 deletions src/errors/errors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,21 @@ func ExampleNew_errorf() {
}
// Output: user "bimmler" (id 17) not found
}

func ExampleJoin() {
err1 := errors.New("err1")
err2 := errors.New("err2")
err := errors.Join(err1, err2)
fmt.Println(err)
if errors.Is(err, err1) {
fmt.Println("err is err1")
}
if errors.Is(err, err2) {
fmt.Println("err is err2")
}
// Output:
// err1
// err2
// err is err1
// err is err2
}
51 changes: 51 additions & 0 deletions src/errors/join.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Copyright 2022 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package errors

// Join returns an error that wraps the given errors.
// Any nil error values are discarded.
// Join returns nil if errs contains no non-nil values.
// The error formats as the concatenation of the strings obtained
// by calling the Error method of each element of errs, with a newline
// between each string.
func Join(errs ...error) error {
n := 0
for _, err := range errs {
if err != nil {
n++
}
}
if n == 0 {
return nil
}
e := &joinError{
errs: make([]error, 0, n),
}
for _, err := range errs {
if err != nil {
e.errs = append(e.errs, err)
}
}
return e
}

type joinError struct {
errs []error
}

func (e *joinError) Error() string {
var b []byte
for i, err := range e.errs {
if i > 0 {
b = append(b, '\n')
}
b = append(b, err.Error()...)
}
return string(b)
}

func (e *joinError) Unwrap() []error {
return e.errs
}
49 changes: 49 additions & 0 deletions src/errors/join_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright 2022 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package errors_test

import (
"errors"
"reflect"
"testing"
)

func TestJoinReturnsNil(t *testing.T) {
if err := errors.Join(); err != nil {
t.Errorf("errors.Join() = %v, want nil", err)
}
if err := errors.Join(nil); err != nil {
t.Errorf("errors.Join(nil) = %v, want nil", err)
}
if err := errors.Join(nil, nil); err != nil {
t.Errorf("errors.Join(nil, nil) = %v, want nil", err)
}
}

func TestJoin(t *testing.T) {
err1 := errors.New("err1")
err2 := errors.New("err2")
for _, test := range []struct {
errs []error
want []error
}{{
errs: []error{err1},
want: []error{err1},
}, {
errs: []error{err1, err2},
want: []error{err1, err2},
}, {
errs: []error{err1, nil, err2},
want: []error{err1, err2},
}} {
got := errors.Join(test.errs...).(interface{ Unwrap() []error }).Unwrap()
if !reflect.DeepEqual(got, test.want) {
t.Errorf("Join(%v) = %v; want %v", test.errs, got, test.want)
}
if len(got) != cap(got) {
t.Errorf("Join(%v) returns errors with len=%v, cap=%v; want len==cap", test.errs, len(got), cap(got))
}
}
}
57 changes: 44 additions & 13 deletions src/errors/wrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import (
// Unwrap returns the result of calling the Unwrap method on err, if err's
// type contains an Unwrap method returning error.
// Otherwise, Unwrap returns nil.
//
// Unwrap returns nil if the Unwrap method returns []error.
func Unwrap(err error) error {
u, ok := err.(interface {
Unwrap() error
Expand All @@ -21,10 +23,11 @@ func Unwrap(err error) error {
return u.Unwrap()
}

// Is reports whether any error in err's chain matches target.
// Is reports whether any error in err's tree matches target.
//
// The chain consists of err itself followed by the sequence of errors obtained by
// repeatedly calling Unwrap.
// The tree consists of err itself, followed by the errors obtained by repeatedly
// calling Unwrap. When err wraps multiple errors, Is examines err followed by a
// depth-first traversal of its children.
//
// An error is considered to match a target if it is equal to that target or if
// it implements a method Is(error) bool such that Is(target) returns true.
Expand All @@ -50,20 +53,31 @@ func Is(err, target error) bool {
if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {
return true
}
// TODO: consider supporting target.Is(err). This would allow
// user-definable predicates, but also may allow for coping with sloppy
// APIs, thereby making it easier to get away with them.
if err = Unwrap(err); err == nil {
switch x := err.(type) {
case interface{ Unwrap() error }:
err = x.Unwrap()
if err == nil {
return false
}
case interface{ Unwrap() []error }:
for _, err := range x.Unwrap() {
if Is(err, target) {
return true
}
}
return false
default:
return false
}
}
}

// As finds the first error in err's chain that matches target, and if one is found, sets
// As finds the first error in err's tree that matches target, and if one is found, sets
// target to that error value and returns true. Otherwise, it returns false.
//
// The chain consists of err itself followed by the sequence of errors obtained by
// repeatedly calling Unwrap.
// The tree consists of err itself, followed by the errors obtained by repeatedly
// calling Unwrap. When err wraps multiple errors, As examines err followed by a
// depth-first traversal of its children.
//
// An error matches target if the error's concrete value is assignable to the value
// pointed to by target, or if the error has a method As(interface{}) bool such that
Expand All @@ -76,6 +90,9 @@ func Is(err, target error) bool {
// As panics if target is not a non-nil pointer to either a type that implements
// error, or to any interface type.
func As(err error, target any) bool {
if err == nil {
return false
}
if target == nil {
panic("errors: target cannot be nil")
}
Expand All @@ -88,17 +105,31 @@ func As(err error, target any) bool {
if targetType.Kind() != reflectlite.Interface && !targetType.Implements(errorType) {
panic("errors: *target must be interface or implement error")
}
for err != nil {
for {
if reflectlite.TypeOf(err).AssignableTo(targetType) {
val.Elem().Set(reflectlite.ValueOf(err))
return true
}
if x, ok := err.(interface{ As(any) bool }); ok && x.As(target) {
return true
}
err = Unwrap(err)
switch x := err.(type) {
case interface{ Unwrap() error }:
err = x.Unwrap()
if err == nil {
return false
}
case interface{ Unwrap() []error }:
for _, err := range x.Unwrap() {
if As(err, target) {
return true
}
}
return false
default:
return false
}
}
return false
}

var errorType = reflectlite.TypeOf((*error)(nil)).Elem()
52 changes: 51 additions & 1 deletion src/errors/wrap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,17 @@ func TestIs(t *testing.T) {
{&errorUncomparable{}, &errorUncomparable{}, false},
{errorUncomparable{}, err1, false},
{&errorUncomparable{}, err1, false},
{multiErr{}, err1, false},
{multiErr{err1, err3}, err1, true},
{multiErr{err3, err1}, err1, true},
{multiErr{err1, err3}, errors.New("x"), false},
{multiErr{err3, errb}, errb, true},
{multiErr{err3, errb}, erra, true},
{multiErr{err3, errb}, err1, true},
{multiErr{errb, err3}, err1, true},
{multiErr{poser}, err1, true},
{multiErr{poser}, err3, true},
{multiErr{nil}, nil, false},
}
for _, tc := range testCases {
t.Run("", func(t *testing.T) {
Expand Down Expand Up @@ -148,6 +159,41 @@ func TestAs(t *testing.T) {
&timeout,
true,
errF,
}, {
multiErr{},
&errT,
false,
nil,
}, {
multiErr{errors.New("a"), errorT{"T"}},
&errT,
true,
errorT{"T"},
}, {
multiErr{errorT{"T"}, errors.New("a")},
&errT,
true,
errorT{"T"},
}, {
multiErr{errorT{"a"}, errorT{"b"}},
&errT,
true,
errorT{"a"},
}, {
multiErr{multiErr{errors.New("a"), errorT{"a"}}, errorT{"b"}},
&errT,
true,
errorT{"a"},
}, {
multiErr{wrapped{"path error", errF}},
&timeout,
true,
errF,
}, {
multiErr{nil},
&errT,
false,
nil,
}}
for i, tc := range testCases {
name := fmt.Sprintf("%d:As(Errorf(..., %v), %v)", i, tc.err, tc.target)
Expand Down Expand Up @@ -223,9 +269,13 @@ type wrapped struct {
}

func (e wrapped) Error() string { return e.msg }

func (e wrapped) Unwrap() error { return e.err }

type multiErr []error

func (m multiErr) Error() string { return "multiError" }
func (m multiErr) Unwrap() []error { return []error(m) }

type errorUncomparable struct {
f []string
}
Expand Down
Loading

0 comments on commit 4a0a2b3

Please sign in to comment.