]> Cypherpunks repositories - gostls13.git/commitdiff
errors: add AsType
authorJulien Cretel <jub0bsinthecloud@gmail.com>
Mon, 29 Sep 2025 16:57:53 +0000 (16:57 +0000)
committerDamien Neil <dneil@google.com>
Tue, 30 Sep 2025 17:22:08 +0000 (10:22 -0700)
Fixes #51945

Change-Id: Icda169782e796578eba728938134a85b5827d3b6
GitHub-Last-Rev: c6ff335ee1ffb6b7975141795a4632a55247299d
GitHub-Pull-Request: golang/go#75621
Reviewed-on: https://go-review.googlesource.com/c/go/+/707235
Reviewed-by: Carlos Amedee <carlos@golang.org>
Reviewed-by: Damien Neil <dneil@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Sean Liao <sean@liao.dev>
api/next/51945.txt [new file with mode: 0644]
doc/next/6-stdlib/99-minor/errors/51945.md [new file with mode: 0644]
src/errors/errors.go
src/errors/example_test.go
src/errors/wrap.go
src/errors/wrap_test.go

diff --git a/api/next/51945.txt b/api/next/51945.txt
new file mode 100644 (file)
index 0000000..7db1f09
--- /dev/null
@@ -0,0 +1 @@
+pkg errors, func AsType[$0 error](error) ($0, bool) #51945
diff --git a/doc/next/6-stdlib/99-minor/errors/51945.md b/doc/next/6-stdlib/99-minor/errors/51945.md
new file mode 100644 (file)
index 0000000..44ac722
--- /dev/null
@@ -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.
index 5059be12ed441f4a9ed2856106a85ee784f9ba79..8b926cfe148c448696750baa12b4ce175d4dc033 100644 (file)
 //
 // 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)
 //     }
 //
index 278df8c7da6e5aa7e6711ccfc9b4fa2ffdb98294..92ef36b1010edbc6d4c66e7e5a050e2a06fc10b1 100644 (file)
@@ -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)
index eec9591dae7b933132b442422cc74b3935b79f65..2ebb951f1de93ddfcadf02cc391680dafb0eb2a9 100644 (file)
@@ -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.
@@ -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
+               }
+       }
+}
index 58ed95fd9a0fc735e7ffe774304ddea1a5ab9e2a..81c795a6bb8b18355ae73f4398c9ac8c755c91df 100644 (file)
@@ -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"}}}
@@ -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}