Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions api/next/51945.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pkg errors, func AsType[$0 error](error) ($0, bool) #51945
2 changes: 2 additions & 0 deletions doc/next/6-stdlib/99-minor/errors/51945.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
The new [AsType] function is a generic version of [As]. It is type-safe, faster,
and, in most cases, easier to use.
10 changes: 5 additions & 5 deletions src/errors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,12 @@
//
// because the former will succeed if err wraps [io/fs.ErrExist].
//
// [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
// [AsType] examines the tree of its argument looking for an error whose
// type matches its type argument. If it succeeds, it returns the
// corresponding value of that type and true. Otherwise, it returns the
// zero value of that type and false. The form
//
// var perr *fs.PathError
// if errors.As(err, &perr) {
// if perr, ok := errors.AsType[*fs.PathError](err); ok {
// fmt.Println(perr.Path)
// }
//
Expand Down
12 changes: 12 additions & 0 deletions src/errors/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,18 @@ func ExampleAs() {
// Failed at path: non-existing
}

func ExampleAsType() {
if _, err := os.Open("non-existing"); err != nil {
if pathError, ok := errors.AsType[*fs.PathError](err); ok {
fmt.Println("Failed at path:", pathError.Path)
} else {
fmt.Println(err)
}
}
// Output:
// Failed at path: non-existing
}

func ExampleUnwrap() {
err1 := errors.New("error1")
err2 := fmt.Errorf("error2: [%w]", err1)
Expand Down
61 changes: 61 additions & 0 deletions src/errors/wrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ func is(err, target error, targetComparable bool) bool {
// 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.
//
// For most uses, prefer [AsType]. As is equivalent to [AsType] but sets its target
// argument rather than returning the matching error and doesn't require its target
// argument to implement error.
//
// The tree consists of err itself, followed by the errors obtained by repeatedly
// calling its Unwrap() error or Unwrap() []error method. When err wraps multiple
// errors, As examines err followed by a depth-first traversal of its children.
Expand Down Expand Up @@ -145,3 +149,60 @@ func as(err error, target any, targetVal reflectlite.Value, targetType reflectli
}

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

// AsType finds the first error in err's tree that matches the type E, and
// if one is found, returns that error value and true. Otherwise, it
// returns the zero value of E and false.
//
// The tree consists of err itself, followed by the errors obtained by
// repeatedly calling its Unwrap() error or Unwrap() []error method. When
// err wraps multiple errors, AsType examines err followed by a
// depth-first traversal of its children.
//
// An error err matches the type E if the type assertion err.(E) holds,
// or if the error has a method As(any) bool such that err.As(target)
// returns true when target is a non-nil *E. In the latter case, the As
// method is responsible for setting target.
func AsType[E error](err error) (E, bool) {
if err == nil {
var zero E
return zero, false
}
var pe *E // lazily initialized
return asType(err, &pe)
}

func asType[E error](err error, ppe **E) (_ E, _ bool) {
for {
if e, ok := err.(E); ok {
return e, true
}
if x, ok := err.(interface{ As(any) bool }); ok {
if *ppe == nil {
*ppe = new(E)
}
if x.As(*ppe) {
return **ppe, true
}
}
switch x := err.(type) {
case interface{ Unwrap() error }:
err = x.Unwrap()
if err == nil {
return
}
case interface{ Unwrap() []error }:
for _, err := range x.Unwrap() {
if err == nil {
continue
}
if x, ok := asType(err, ppe); ok {
return x, true
}
}
return
default:
return
}
}
}
126 changes: 126 additions & 0 deletions src/errors/wrap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,123 @@ func TestAsValidation(t *testing.T) {
}
}

func TestAsType(t *testing.T) {
var errT errorT
var errP *fs.PathError
type timeout interface {
Timeout() bool
error
}
_, errF := os.Open("non-existing")
poserErr := &poser{"oh no", nil}

testAsType(t,
nil,
errP,
false,
)
testAsType(t,
wrapped{"pitied the fool", errorT{"T"}},
errorT{"T"},
true,
)
testAsType(t,
errF,
errF,
true,
)
testAsType(t,
errT,
errP,
false,
)
testAsType(t,
wrapped{"wrapped", nil},
errT,
false,
)
testAsType(t,
&poser{"error", nil},
errorT{"poser"},
true,
)
testAsType(t,
&poser{"path", nil},
poserPathErr,
true,
)
testAsType(t,
poserErr,
poserErr,
true,
)
testAsType(t,
errors.New("err"),
timeout(nil),
false,
)
testAsType(t,
errF,
errF.(timeout),
true)
testAsType(t,
wrapped{"path error", errF},
errF.(timeout),
true,
)
testAsType(t,
multiErr{},
errT,
false,
)
testAsType(t,
multiErr{errors.New("a"), errorT{"T"}},
errorT{"T"},
true,
)
testAsType(t,
multiErr{errorT{"T"}, errors.New("a")},
errorT{"T"},
true,
)
testAsType(t,
multiErr{errorT{"a"}, errorT{"b"}},
errorT{"a"},
true,
)
testAsType(t,
multiErr{multiErr{errors.New("a"), errorT{"a"}}, errorT{"b"}},
errorT{"a"},
true,
)
testAsType(t,
multiErr{wrapped{"path error", errF}},
errF.(timeout),
true,
)
testAsType(t,
multiErr{nil},
errT,
false,
)
}

type compError interface {
comparable
error
}

func testAsType[E compError](t *testing.T, err error, want E, wantOK bool) {
t.Helper()
name := fmt.Sprintf("AsType[%T](Errorf(..., %v))", want, err)
t.Run(name, func(t *testing.T) {
got, gotOK := errors.AsType[E](err)
if gotOK != wantOK || got != want {
t.Fatalf("got %v, %t; want %v, %t", got, gotOK, want, wantOK)
}
})
}

func BenchmarkIs(b *testing.B) {
err1 := errors.New("1")
err2 := multiErr{multiErr{multiErr{err1, errorT{"a"}}, errorT{"b"}}}
Expand All @@ -260,6 +377,15 @@ func BenchmarkAs(b *testing.B) {
}
}

func BenchmarkAsType(b *testing.B) {
err := multiErr{multiErr{multiErr{errors.New("a"), errorT{"a"}}, errorT{"b"}}}
for range b.N {
if _, ok := errors.AsType[errorT](err); !ok {
b.Fatal("AsType failed")
}
}
}

func TestUnwrap(t *testing.T) {
err1 := errors.New("1")
erra := wrapped{"wrap 2", err1}
Expand Down