Skip to content

Commit 8cb147f

Browse files
committed
errors: add AsType
Fixes #51945
1 parent 15fbe34 commit 8cb147f

File tree

5 files changed

+231
-0
lines changed

5 files changed

+231
-0
lines changed

api/next/51945.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pkg errors, func AsType[$0 error](error) ($0, bool) #51945

src/errors/errors.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@
4141
//
4242
// because the former will succeed if err wraps [io/fs.ErrExist].
4343
//
44+
// [AsType] examines the tree of its argument looking for an error whose
45+
// type matches its type argument. If it succeeds, it returns the
46+
// corresponding value of that type and true. Otherwise, it returns the
47+
// zero value of that type and false.
48+
//
4449
// [As] examines the tree of its first argument looking for an error that can be
4550
// assigned to its second argument, which must be a pointer. If it succeeds, it
4651
// performs the assignment and returns true. Otherwise, it returns false. The form
@@ -57,6 +62,7 @@
5762
// }
5863
//
5964
// because the former will succeed if err wraps an [*io/fs.PathError].
65+
// Prefer [AsType] instead.
6066
package errors
6167

6268
// New returns an error that formats as the given text.

src/errors/example_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,44 @@ func ExampleAs() {
102102
// Failed at path: non-existing
103103
}
104104

105+
func ExampleAsType() {
106+
if _, err := os.Open("non-existing"); err != nil {
107+
if pathError, ok := errors.AsType[*fs.PathError](err); ok {
108+
fmt.Println("Failed at path:", pathError.Path)
109+
} else {
110+
fmt.Println(err)
111+
}
112+
}
113+
// Output:
114+
// Failed at path: non-existing
115+
}
116+
117+
type timeout interface{ Timeout() bool }
118+
119+
// In some cases, the result's desired type is an interface other than
120+
// error. Consider the interface type declared as follows:
121+
//
122+
// type timeout interface{ Timeout() bool }
123+
//
124+
// Because timeout doesn't satisfy the error type constraint, you cannot
125+
// simply use timeout as type argument. Instead, use an interface type
126+
// that embeds both timeout and error.
127+
func ExampleAsType_other() {
128+
if _, err := os.Open("non-existing"); err != nil {
129+
type target interface {
130+
timeout
131+
error
132+
}
133+
if t, ok := errors.AsType[target](err); ok {
134+
fmt.Println("Timed out:", t.Timeout())
135+
} else {
136+
fmt.Println(err)
137+
}
138+
}
139+
// Output:
140+
// Timed out: false
141+
}
142+
105143
func ExampleUnwrap() {
106144
err1 := errors.New("error1")
107145
err2 := fmt.Errorf("error2: [%w]", err1)

src/errors/wrap.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,10 @@ func is(err, target error, targetComparable bool) bool {
9494
//
9595
// As panics if target is not a non-nil pointer to either a type that implements
9696
// error, or to any interface type.
97+
//
98+
// [AsType] is semantically equivalent to [As], with one caveat: the argument to [As]
99+
// needs not implement the error interface. Consider using [AsType] instead of [As];
100+
// it's more ergonomic, type-safe, and performant.
97101
func As(err error, target any) bool {
98102
if err == nil {
99103
return false
@@ -145,3 +149,59 @@ func as(err error, target any, targetVal reflectlite.Value, targetType reflectli
145149
}
146150

147151
var errorType = reflectlite.TypeOf((*error)(nil)).Elem()
152+
153+
// AsType finds the first error in err's tree that matches the type E, and
154+
// if one is found, returns that error value and true. Otherwise, it
155+
// returns the zero value of E and false.
156+
//
157+
// The tree consists of err itself, followed by the errors obtained by
158+
// repeatedly calling its Unwrap() error or Unwrap() []error method. When
159+
// err wraps multiple errors, AsType examines err followed by a
160+
// depth-first traversal of its children.
161+
//
162+
// An error e matches the type E if the type assertion e.(E) holds, or if
163+
// the error has a method As(any) bool such that e.As(target) returns
164+
// true when target is a non-nil *E. In the latter case, the As method is
165+
// responsible for setting target.
166+
func AsType[E error](err error) (_ E, ok bool) {
167+
if err == nil {
168+
return
169+
}
170+
var pe *E // lazily initialized
171+
return asType(err, &pe)
172+
}
173+
174+
func asType[E error](err error, ppe **E) (_ E, ok bool) {
175+
for {
176+
if e, ok := err.(E); ok {
177+
return e, true
178+
}
179+
if x, ok := err.(interface{ As(any) bool }); ok {
180+
if *ppe == nil {
181+
*ppe = new(E)
182+
}
183+
if x.As(*ppe) {
184+
return **ppe, true
185+
}
186+
}
187+
switch x := err.(type) {
188+
case interface{ Unwrap() error }:
189+
err = x.Unwrap()
190+
if err == nil {
191+
return
192+
}
193+
case interface{ Unwrap() []error }:
194+
for _, err := range x.Unwrap() {
195+
if err == nil {
196+
continue
197+
}
198+
if x, ok := asType(err, ppe); ok {
199+
return x, true
200+
}
201+
}
202+
return
203+
default:
204+
return
205+
}
206+
}
207+
}

src/errors/wrap_test.go

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,123 @@ func TestAsValidation(t *testing.T) {
239239
}
240240
}
241241

242+
func TestAsType(t *testing.T) {
243+
var errT errorT
244+
var errP *fs.PathError
245+
type timeout interface {
246+
Timeout() bool
247+
error
248+
}
249+
_, errF := os.Open("non-existing")
250+
poserErr := &poser{"oh no", nil}
251+
252+
testAsType(t,
253+
nil,
254+
errP,
255+
false,
256+
)
257+
testAsType(t,
258+
wrapped{"pitied the fool", errorT{"T"}},
259+
errorT{"T"},
260+
true,
261+
)
262+
testAsType(t,
263+
errF,
264+
errF,
265+
true,
266+
)
267+
testAsType(t,
268+
errT,
269+
errP,
270+
false,
271+
)
272+
testAsType(t,
273+
wrapped{"wrapped", nil},
274+
errT,
275+
false,
276+
)
277+
testAsType(t,
278+
&poser{"error", nil},
279+
errorT{"poser"},
280+
true,
281+
)
282+
testAsType(t,
283+
&poser{"path", nil},
284+
poserPathErr,
285+
true,
286+
)
287+
testAsType(t,
288+
poserErr,
289+
poserErr,
290+
true,
291+
)
292+
testAsType(t,
293+
errors.New("err"),
294+
timeout(nil),
295+
false,
296+
)
297+
testAsType(t,
298+
errF,
299+
errF.(timeout),
300+
true)
301+
testAsType(t,
302+
wrapped{"path error", errF},
303+
errF.(timeout),
304+
true,
305+
)
306+
testAsType(t,
307+
multiErr{},
308+
errT,
309+
false,
310+
)
311+
testAsType(t,
312+
multiErr{errors.New("a"), errorT{"T"}},
313+
errorT{"T"},
314+
true,
315+
)
316+
testAsType(t,
317+
multiErr{errorT{"T"}, errors.New("a")},
318+
errorT{"T"},
319+
true,
320+
)
321+
testAsType(t,
322+
multiErr{errorT{"a"}, errorT{"b"}},
323+
errorT{"a"},
324+
true,
325+
)
326+
testAsType(t,
327+
multiErr{multiErr{errors.New("a"), errorT{"a"}}, errorT{"b"}},
328+
errorT{"a"},
329+
true,
330+
)
331+
testAsType(t,
332+
multiErr{wrapped{"path error", errF}},
333+
errF.(timeout),
334+
true,
335+
)
336+
testAsType(t,
337+
multiErr{nil},
338+
errT,
339+
false,
340+
)
341+
}
342+
343+
type compError interface {
344+
comparable
345+
error
346+
}
347+
348+
func testAsType[E compError](t *testing.T, err error, want E, wantOK bool) {
349+
t.Helper()
350+
name := fmt.Sprintf("AsType[%T](Errorf(..., %v))", want, err)
351+
t.Run(name, func(t *testing.T) {
352+
got, gotOK := errors.AsType[E](err)
353+
if gotOK != wantOK || got != want {
354+
t.Fatalf("got %v, %t; want %v, %t", got, gotOK, want, wantOK)
355+
}
356+
})
357+
}
358+
242359
func BenchmarkIs(b *testing.B) {
243360
err1 := errors.New("1")
244361
err2 := multiErr{multiErr{multiErr{err1, errorT{"a"}}, errorT{"b"}}}
@@ -260,6 +377,15 @@ func BenchmarkAs(b *testing.B) {
260377
}
261378
}
262379

380+
func BenchmarkAsType(b *testing.B) {
381+
err := multiErr{multiErr{multiErr{errors.New("a"), errorT{"a"}}, errorT{"b"}}}
382+
for range b.N {
383+
if _, ok := errors.AsType[errorT](err); !ok {
384+
b.Fatal("AsType failed")
385+
}
386+
}
387+
}
388+
263389
func TestUnwrap(t *testing.T) {
264390
err1 := errors.New("1")
265391
erra := wrapped{"wrap 2", err1}

0 commit comments

Comments
 (0)