-
Notifications
You must be signed in to change notification settings - Fork 18.4k
Description
Proposal Details
The original error inspection proposal contained the following paragraph:
We recognize that there are situations that
Is
andAs
don’t handle
well. Sometimes callers want to perform multiple checks against the
same error, like comparing against more than one sentinel value.
Although these can be handled by multiple calls to Is or As, each call
walks the chain separately, which could be wasteful. Sometimes, a
package will provide a function to retrieve information from an
unexported error type, as in this old version of gRPC's status.Code
function.
Is
andAs
cannot help here at all. For cases like these, programs
can traverse the error chain directly.
Traversing the error chain multiple times to call Is
is wasteful,
and it's not entirely trivial to do it oneself. In fact, the way to
iterate through errors has changed recently with the advent of
multiple error wrapping.
If many people had been traversing the error chain directly, their
code would now be insufficient.
If we're checking for some particular code in some error type, it's
tempting (but wrong) to write something like this:
var errWithCode *errorWithCode
if errors.As(err, &errWithCode) {
switch err.Code {
case CodeFoo:
case CodeBar:
...
}
}
The above code is wrong because there might be several errors in the
error tree of type *errorWithCode
, but we will only ever see the
first one. It would be possible to abuse the Is
method to consider
only the Code
field when comparing errorWithCode
types, but that
seems like an abuse: Is
is really intended for identical errors,
not errors that one might sometimes wish to consider equivalent.
With the advent of iterators, we now have a natural way to design an
API that efficiently provides access to all the nested errors without
requiring creation of an intermediate slice.
I propose the following two additions to the errors
package:
package errors
import "iter"
// Iter returns an iterator that iterates over all the non-nil errors in
// err's tree, including err itself. See [As] for the definition of the
// tree.
func Iter(err error) iter.Iter[error]
// IterAs is like [Iter] except that the iterator produces
// only items that match type T.
func IterAs[T any](err error) iter.Iter[T]
Technically only IterAs
is necessary, because IterAs[error]
is entirely equivalent to Iter
, but Iter
is more efficient and IterAs
is easily implemented in terms of it.
Both Is
and As
are easily and efficiently implemented in terms of the above API.
I consider IterAs
to be worthwhile because it's convenient and type-safe to use, and it hides the not-entirely-trivial interface check behind the API.
The flawed code above could now be written as follows, correctly this time, and slightly shorter than the original:
for err := range errors.IterAs[*errorWithCode] {
switch err.Code {
case CodeFoo:
case CodeBar:
...
}
}
I've pushed a straw-man implementation at https://go-review.googlesource.com/c/go/+/573357