Skip to content

Commit 73f4ce9

Browse files
committed
errors: add AsType
Fixes #51945
1 parent 15fbe34 commit 73f4ce9

File tree

6 files changed

+206
-5
lines changed

6 files changed

+206
-5
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
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
The new [AsType] function is a more ergonomic alternative to [As].

src/errors/errors.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,12 @@
4141
//
4242
// because the former will succeed if err wraps [io/fs.ErrExist].
4343
//
44-
// [As] examines the tree of its first argument looking for an error that can be
45-
// assigned to its second argument, which must be a pointer. If it succeeds, it
46-
// performs the assignment and returns true. Otherwise, it returns false. The form
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. The form
4748
//
48-
// var perr *fs.PathError
49-
// if errors.As(err, &perr) {
49+
// if perr, ok := errors.AsType[*fs.PathError](err); ok {
5050
// fmt.Println(perr.Path)
5151
// }
5252
//

src/errors/example_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,18 @@ 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+
105117
func ExampleUnwrap() {
106118
err1 := errors.New("error1")
107119
err2 := fmt.Errorf("error2: [%w]", err1)

src/errors/wrap.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@ func is(err, target error, targetComparable bool) bool {
8080
// As finds the first error in err's tree that matches target, and if one is found, sets
8181
// target to that error value and returns true. Otherwise, it returns false.
8282
//
83+
// For most uses, prefer [AsType]. As is equivalent to [AsType] but sets its target
84+
// argument rather than returning the matching error and doesn't require its target
85+
// argument to implement error.
86+
//
8387
// The tree consists of err itself, followed by the errors obtained by repeatedly
8488
// calling its Unwrap() error or Unwrap() []error method. When err wraps multiple
8589
// 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
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 err matches the type E if the type assertion err.(E) holds,
163+
// or if the error has a method As(any) bool such that err.As(target)
164+
// returns true when target is a non-nil *E. In the latter case, the As
165+
// method is responsible for setting target.
166+
func AsType[E error](err error) (E, bool) {
167+
if err == nil {
168+
var zero E
169+
return zero, false
170+
}
171+
var pe *E // lazily initialized
172+
return asType(err, &pe)
173+
}
174+
175+
func asType[E error](err error, ppe **E) (_ E, _ bool) {
176+
for {
177+
if e, ok := err.(E); ok {
178+
return e, true
179+
}
180+
if x, ok := err.(interface{ As(any) bool }); ok {
181+
if *ppe == nil {
182+
*ppe = new(E)
183+
}
184+
if x.As(*ppe) {
185+
return **ppe, true
186+
}
187+
}
188+
switch x := err.(type) {
189+
case interface{ Unwrap() error }:
190+
err = x.Unwrap()
191+
if err == nil {
192+
return
193+
}
194+
case interface{ Unwrap() []error }:
195+
for _, err := range x.Unwrap() {
196+
if err == nil {
197+
continue
198+
}
199+
if x, ok := asType(err, ppe); ok {
200+
return x, true
201+
}
202+
}
203+
return
204+
default:
205+
return
206+
}
207+
}
208+
}

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)