Skip to content

proposal: errors: add All and AllAs iterators #66455

@rogpeppe

Description

@rogpeppe

Proposal Details

The original error inspection proposal contained the following paragraph:

We recognize that there are situations that Is and As 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 and As 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions