Currently in 1.18 and before, when using the errors.As method, an error type you would like to write into must be predeclared before calling the function. For example:

var myErr *MyCustomError
if errors.As(err, &myErr) {
  // handle myErr
}

This can makes control flow around handling errors "unergonomic".

I'd propose that a new, type parameterized method be added to the errors package in 1.19:

func IsA[T error](err error) (T, bool) {
    var isErr T
    if errors.As(err, &isErr) {
        return isErr, true
    }

    var zero T
    return zero, false
}

This enables more "ergonomic" usage as follows:

err := foo()
if err != nil {
    if myErr, ok := errors.IsA[*MyCustomError](err); ok {
        // handle myErr
    } else if otherErr, ok := errors.IsA[*OtherError](err); ok {
        // handle otherErr
    }

    // handle everything else
}

instead of

err := foo()
if err != nil {
    var myErr *MyCustomError
    if errors.As(err, &myErr) {
        // handle customer error
    }

    var otherErr *OtherError
    if errors.As(err, &otherErr) {
        // handle other error
    }

    // handle everything else
}
````

This change would reduce the overall LOC needed for handling custom errors, imo improves readability of the function, as well as scopes the errors to the `if` blocks they are needed in.

Naming is hard so `IsA` might be better replaced with something else.

**Comment From: seankhliao**

Interesting, but changing the signature of `errors.As` is not a backwards compatible change.
It will have to be a new function.

**Comment From: slnt**

> Interesting, but changing the signature of `errors.As` is not a backwards compatible change. It will have to be a new function.

Yeah, unfortunate. I think `if customerErr, ok := errors.IsA[*CustomError](err); ok` reads quite nicely. It would be implemented pretty simply in terms of `errors.As`:

```go
func IsA[T error](err error) (T, bool) {
    var isErr T
    if errors.As(err, &isErr) {
        return isErr, true
    }

    var zero T
    return zero, false
}

Comment From: ianlancetaylor

CC @jba @neild

(My vague recollection is that this was considered and rejected when errors.As was introduced, even beyond the fact that at the time type parameters did not yet exist.)

Comment From: neild

@rsc made the case in the original proposal issue that a type-parameterized version of errors.As would not be an improvement: https://github.com/golang/go/issues/29934#issuecomment-490091428

@dsnet also pointed out that the non-type-parameterized version of As can be used in a switch, while the type-parameterized one cannot: https://github.com/golang/go/issues/29934#issuecomment-460093518

I don't recall if there were any other arguments against a type-parameterized As (aside from, obviously, the fact that type parameters didn't exist at the time).

I personally think that a type-parameterized As seems fairly reasonable, although the practical benefit over the current As seems small. It would need a good name. I don't know what that name would be. IsA does not seem right. Under the current design, "Is" is an enhanced form of equality and "As" is an enhanced form of type assertion; blurring that distinction would add confusion.

An argument against adding a type-parameterized As is that the benefit does not justify the cost in API churn and user confusion. I don't have a strong opinion on whether the benefits do outweigh the costs.

Comment From: DeedleFake

I personally think that a type-parameterized As seems fairly reasonable, although the practical benefit over the current As seems small. It would need a good name. I don't know what that name would be. IsA does not seem right. Under the current design, "Is" is an enhanced form of equality and "As" is an enhanced form of type assertion; blurring that distinction would add confusion.

AsA() could work, though it's a little oddly close to the existing one. if pe, ok := errors.AsA[*os.PathError](err); ok { ... } reads pretty well to me, though it should technically be 'an *os.PathError', not 'a'.

New proposal: Automatically alias all functions with names that match ([a-z0-9])A$ to ${1}An.

Comment From: zigo101

I like this proposal, but I fell the call errors.IsA[*MyCustomError](err) is not very natural. If would be good if we could pass type arguments to unnamed value parameters (_) of type parameter types. For example,

func IsA[T error](err error, _ T) (T, bool) {
    var isErr T
    if errors.As(err, &isErr) {
        return isErr, true
    }

    var zero T
    return zero, false
}

err := foo()
if err != nil {
    if myErr, ok := errors.IsA(err, MyCustomError); ok {
        // handle myErr
    } else if otherErr, ok := errors.IsA(err, OtherError); ok {
        // handle otherErr
    }

    // handle everything else
}

Comment From: zigo101

Or more generally, it would be good to pass type arguments to any value parameters of type parameter types. It is equivalent to pass zero values of the type arguments.

[Edit] This could unify built-in generic functions and custom ones to some extent.

func new[T any](T) *T

is much better than the illogical fake declaration:

func new(Type) *Type

Comment From: earthboundkid

I don't see much benefit to duplicating the existing API. This also implies a similar change to eg json.Marshal etc. We would end up with duplicate functions throughout the standard library. And as noted this doesn't add any type safety; it is just more convenient, although even that is debatable. I think generics should be reserved for areas where they either add real type safety or big convenience, and cases like this of minor convenience can stay as they are.

Comment From: neild

And as noted this doesn't add any type safety

That's not quite true: This does add type safety at the language level, rather than leaving it to a go vet check. Under this proposal, this code would not be valid:

type MyError struct{}
func (*MyError) Error() string { return "MyError" }

func main() {
    var err error
    m, ok := errors.AsA[MyError](err) // MyError does not implement error (Error method has pointer receiver)
    fmt.Println(m, ok)
}

The equivalent errors.As call is a run-time panic or go vet failure:

second argument to errors.As must be a non-nil pointer to either a type that implements error, or to any interface type

Comment From: slnt

A sidenote is the original error inspection draft design includes this blurb:

Here we are assuming the use of the contracts draft design to make errors.As explicitly polymorphic: func As(type E)(err error) (e E, ok bool)

so its not I guess a new idea

Comment From: GeorgeMac

Love this, I stumbled across this pattern, made a quick blog post about it. Then, of course, that is when I find all the proposals 😂.

I think you can get it down to this terse definition:

func AsA[E error](err error) (e E, _ bool) {
    return e, errors.As(err, &e)
}

Comment From: phenpessoa

There are a few benefits of having a generic version of As, that I don't think have been brought up here. I mentioned them in: https://github.com/golang/go/issues/56949

User @Jorropo wrote the function this way:

func AsOf[E error](err error) (E, bool) {
    var ptrToE *E
    for err != nil {
        if e, ok := err.(E); ok {
            return e, true
        }
        if x, ok := err.(interface{ As(any) bool }); ok {
            if ptrToE == nil {
                ptrToE = new(E)
            }
            if x.As(ptrToE) {
                return *ptrToE, true
            }
        }
        err = Unwrap(err)
    }
    var zero E
    return zero, false
}]

Note that it does not use the current As implementation. The benefits of this implementation are:

  • No usage of reflection
  • No runtime panic possibility
  • An allocation free path
  • Compile time type safety
  • Faster

Comment From: joncalhoun

All of these proposals are looking for a way to return the error as a specific type, which I agree is nice if possible, but have there been any discussions around simply improving errors.As with generics? Specifically:

func As[T error](err error, target *T) bool {
    // This is used to show the functionality works the same
    return errors.As(err, target)
}

At first glance this appears to be a breaking change, but passing anything that doesn't meet this criteria into the As function would result in a panic. This change would alert people of the issue sooner (during compile time) rather than at runtime.

It is also very possible I am missing an edge case.

I have looked, but haven't found an issue that discusses this approach. Please let me know if one exists.

Comment From: neild

At first glance this appears to be a breaking change

This would break the following:

var _ = errors.As

Comment From: joncalhoun

This would break the following:

var _ = errors.As

Darn, you are right. I missed that case. Kinda sucks though, because the change would absolutely help with bugs that aren't discovered until testing or some other runtime occurrence.

Comment From: mkielar

64629 was closed as duplicate, so let me advertise my idea here. Perhaps, instead of adding new IsA function, it would be easier to add something that actually produces that double pointer? Like this:

func AsTarget[T error](err T) *T {
    p1 := &err
    return p1
}

Then we could:

if errors.As(err, AsTarget(&MyErr{})) {
    ...
}

Comment From: earthboundkid

You couldn’t use the value, so it seems like it wouldn’t be useful most of the time.

Comment From: mkielar

Oooh, okay, now I get it. I'm new to go, and I didn't RTFM, so that's on me. I missed the fact that error.As actually sets the target to the value of the error it finds (if it finds it). My idea still holds when one doesn't need that value (which is rare, probably) and makes little sense in a wider context.

Learned something today, thanks @carlmjohnson.

Comment From: mitar

@mkielar: If you do not need value, you use errors.Is.

Comment From: neilotoole

Per (closed) duplicate proposal #64771, I advocate for the name errors.Has. Implementation would look like:

// Has returns true if err, or an error in its error tree, matches error type E.
// An error is considered a match by the rules of [errors.As].
func Has[E error](err error) bool {
  return errors.As(err, new(E))
}

Comment From: earthboundkid

A) It's an Is check, not really an As check, which is less useful. B) That would be very prone to accidental misuse in which the type system would infer error instead of a concrete error type.

Comment From: earthboundkid

  err = container.RunExec(ctx, s.dockerCli, target.ID, exec)
  if errors.Has[*cli.StatusError](err) {
    return sterr.StatusCode, nil
  }
  return 0, err

sterr.StatusCode won't work because sterr is never declared.

Comment From: imax9000

Until this gets into the stdlib, I'm using a simple wrapper around the whole errors package: https://pkg.go.dev/github.com/imax9000/errors

Comment From: jalaziz

In our codebase, we've noticed the need for two different methods: * func AsA[E error](err error) (E, bool) * func IsA[E error](err error) bool

AsA can generally be used everyone IsA is used by ignoring the first return value, except for switch statements. The value of IsA is that it matches on types as compared to errors.Is which will generally fail unless the error instances are exactly equal (or the error has an Is method that overrides the default behavior).

I doubt both methods would get included in the standard library, but besides added type safety and convenience, I've found that both methods make for much cleaner and simpler error-checking code.

Comment From: jub0bs

@slnt FWIW, I've just released jub0bs/errutil, a small utility package that exports the following function:

func Find[T error](err error) (T, bool)

Find has the same signature as your IsA but it's more efficient, since its implementation is generic all the way down instead of relying on errors.As.

Comment From: jub0bs

In the wake of https://go.dev/blog/error-syntax, perhaps we should revive

  • this proposal, or
  • https://github.com/golang/go/issues/56949, or
  • https://github.com/golang/go/issues/64771?

A generic function in the spirit of errors.As goes a long way in terms of ergonomics, type safety, and performance.

Comment From: apparentlymart

Although I agreed with the argument that this didn't seem to add much for type checking or ergonomic usage -- in particular, it's still not possible to write the entire error-projection mess inside the expression of a case clause in a switch statement -- the performance argument seems considerably more compelling1.

If performance were the only motivation then we could presumably get that same benefit with a signature more like the current errors.As:

func GenericAsBikeshed1[T any](err error, target *T) bool

...which would then make it (more or less) a drop-in replacement for the current errors.As, albeit not strictly compatible enough to be able to avoid adding a new symbol.


Let's compare how these different options look with if statements:

// Base case: errors.As in its current form
var specialErr *ErrSpecial
if errors.As(err, &specialErr) {
    // Do something with specialErr
}
// Generic version of errors.As with a similar signature
var specialErr *ErrSpecial
if errors.GenericAsBikeshed1(err, &specialErr) {
    // Do something with specialErr
}
// Generic version with two results instead of writing through a pointer.
if specialErr, ok := errors.GenericAsBikeshed2[*ErrSpecial](err); ok {
    // Do something with specialErr
}

This shape keeps the temporary typed-error value scoped to the error-handling block, which is nice.

My brain got a little caught up on the oddity of using a variable named ok to represent "there is an error" 😬 but I expect I'd get over that before too long, and of course that's a decision each caller can make separately anyway.


Now with switch statements:

// Base case: errors.As in its current form
var specialErr *ErrSpecial
switch {
case errors.As(err, &specialErr):
    // Do something with specialErr
default:
    // ...
}
// Generic version of errors.As with a similar signature
var specialErr *ErrSpecial
switch {
case errors.GenericAsBikeshed1(err, &specialErr):
    // Do something with specialErr
default:
    // ...
}

As others have noted, it doesn't seem like a switch/case usage of errors.GenericAddBikeshed2 is possible, because case doesn't support the simple-statement; predicate form that's allowed in if. I think the closest we can get is:

// Generic version with two results instead of writing through a pointer.
var specialErr *ErrSpecial
switch specialErr, ok := errors.GenericAsBikeshed2[*ErrSpecial](err); ok {
case true
    // Do something with specialErr
default:
    // specialErr is its zero value here
}

...but this is just a very strange way of spelling an if statement, so not particularly useful.


I don't personally think the lack of working with switch/case is necessarily disqualifying, but since the switch/case usage of errors.As is typically used as the modern replacement for a direct type switch over the err I think folks do still like to be able to write it out that way.

Although this would of course be an entirely separate proposal, I wonder if there's any chance of a future language change to support something like the simple-statement form in the case clauses of a boolean switch:

switch {
case specialErr, ok := errors.GenericAsBikeshed2[*ErrSpecial](err); ok:
    // Do something with specialErr
default:
    // specialErr is its zero value here
}

Exactly what I wrote above seems likely to be ambiguous though, so probably it would need different syntax? I'm not sure. (This also reminds me about https://github.com/golang/go/issues/67316, https://github.com/golang/go/issues/67372, and https://github.com/golang/go/issues/70169, which were all more elaborate language changes to support type-switch-like handling of errors.)

If we thought that something like the above might become possible in future then for me that would remove my one reservation about the generic form with two results, rather than the generic form with a pointer. But if we wanted to rely on that here then we'd probably want to discuss it in a separate proposal issue first. 🤔

Comment From: jub0bs

@apparentlymart I don't think that the lack of language support for simple statements in case clauses can be a deciding factor between errors.As, errors.GenericAsBikeshed, errors.GenericAsBikeshed2; the situation is about as bad for all three. I do wish for such support, though, but I don't know how easy such an addition to the language would be.

In terms of pure performance, though, I would argue in favour of errors.GenericAsBikeshed2 (which exists in the form of errutil.Find in my library); see the benchmark results in that repo's README.

Comment From: imax9000

TBH, all the examples you've shown look like weird ways to write an if statement 😅 In particular, they don't offer any benefit in checking for multiple types of errors.

Bikeshedding: GenericAsBikeshed2 is the most ergonomic option and I'm actively using it (my implementation is linked in previous comments)

Comment From: apparentlymart

All but the one I marked as "a weird way to write an if statement" can support checking for multiple errors. I just didn't include that in the examples because I assumed readers would understand how to write multiple case statements there. Indeed, the example using the current errors.As already works today and is already sometimes used in situations where there are multiple error types to match. The final example cannot because the matching expression is in the switch clause rather than in each separate case clause separately.

However, if there is consensus that using switch/case for handling multiple error cases is not a priority then indeed I would agree that "GenericAsBikeshed2" seems like the best fit for an if statement because it only requires a single statement and so the typed error value can be scoped only to the child block.


Performance-wise I'm not seeing any reason why bikeshed1 and bikeshed2 should have materially different performance if written generically rather than with reflection or similar. So it seems like the ergonomic question is the main decider after all, unless I've missed why writing through a pointer is significantly slower than returning the same value that would've been written. (I have admittedly not actually measured it.)

Comment From: jub0bs

@apparentlymart

Performance-wise I'm not seeing any reason why bikeshed1 and bikeshed2 should have materially different performance if written generically rather than with reflection or similar.

Reflection tends to be slow. A generic, reflection-free version of errors.As is bound to be faster. Bear in mind that errors.As not only relies on reflection, but does so recursively.

                                                                     │   errors.As   │              errutil.As              │
                                                                     │    sec/op     │    sec/op     vs base                │
All/c=nil_error,_nil_target-4                                           5.452n ± 76%   2.507n ± 25%  -54.03% (p=0.000 n=10)
All/c=nil_error,_non-nil_target-4                                       35.25n ± 20%   32.48n ±  9%        ~ (p=0.218 n=10)
All/c=no_match-4                                                        90.08n ± 13%   36.90n ±  9%  -59.03% (p=0.000 n=10)
All/c=simple_match-4                                                   112.95n ± 16%   34.42n ± 11%  -69.53% (p=0.000 n=10)
All/c=aser-4                                                            91.87n ± 13%   39.82n ±  9%  -56.65% (p=0.000 n=10)
All/c=wrapper_that_wraps_nil_error-4                                    95.38n ± 10%   30.92n ±  9%  -67.58% (p=0.000 n=10)
All/c=wrapper_that_contains_match-4                                    112.75n ±  1%   32.56n ±  8%  -71.12% (p=0.000 n=10)
All/c=deeply_nested_wrapper_that_contains_match-4                      138.75n ± 12%   53.67n ± 12%  -61.32% (p=0.000 n=10)
All/c=wrapper_that_contains_aser-4                                     106.90n ± 13%   43.28n ± 11%  -59.51% (p=0.000 n=10)
All/c=empty_joiner-4                                                   103.40n ± 11%   42.15n ± 30%  -59.24% (p=0.000 n=10)
All/c=joiner_that_contains_nil-4                                        95.14n ±  9%   37.22n ± 13%  -60.88% (p=0.000 n=10)
All/c=joiner_that_contains_nil_and_match-4                             139.45n ± 13%   42.84n ± 11%  -69.28% (p=0.000 n=10)
All/c=joiner_that_contains_non-nil_and_match-4                         146.50n ± 10%   50.35n ± 13%  -65.63% (p=0.000 n=10)
All/c=joiner_that_contains_match_and_non-nil-4                         123.95n ± 16%   41.90n ± 15%  -66.20% (p=0.000 n=10)
All/c=joiner_that_contains_two_matches-4                               122.10n ± 16%   41.54n ± 14%  -65.98% (p=0.000 n=10)
All/c=deeply_nested_joiner_that_contains_non-nil_and_three_matches-4   120.70n ± 14%   41.01n ± 16%  -66.02% (p=0.000 n=10)
All/c=mix_of_wrappers_and_joiners-4                                    155.70n ± 71%   39.39n ±  7%  -74.70% (p=0.000 n=10)
All/c=mix_of_wrappers_and_joiners_that_contains_asers-4                111.90n ±  2%   43.35n ±  3%  -61.26% (p=0.000 n=10)
All/c=joiner_that_contains_many_false_asers-4                           329.1n ±  5%   162.4n ±  5%  -50.65% (p=0.000 n=10)
geomean                                                                 97.37n         37.06n        -61.94%

As for why bikeshed2 would be faster than bikeshed1, it gives rise to fewer heap allocations:

                                                                     │  errutil.As  │              errutil.Find               │
                                                                     │  allocs/op   │ allocs/op   vs base                     │
All/c=nil_error,_nil_target-4                                          0.000 ± 0%     0.000 ± 0%         ~ (p=1.000 n=10) ¹
All/c=nil_error,_non-nil_target-4                                      1.000 ± 0%     0.000 ± 0%  -100.00% (p=0.000 n=10)
All/c=no_match-4                                                       1.000 ± 0%     0.000 ± 0%  -100.00% (p=0.000 n=10)
All/c=simple_match-4                                                   1.000 ± 0%     0.000 ± 0%  -100.00% (p=0.000 n=10)
All/c=aser-4                                                           1.000 ± 0%     1.000 ± 0%         ~ (p=1.000 n=10) ¹
All/c=wrapper_that_wraps_nil_error-4                                   1.000 ± 0%     0.000 ± 0%  -100.00% (p=0.000 n=10)
All/c=wrapper_that_contains_match-4                                    1.000 ± 0%     0.000 ± 0%  -100.00% (p=0.000 n=10)
All/c=deeply_nested_wrapper_that_contains_match-4                      1.000 ± 0%     0.000 ± 0%  -100.00% (p=0.000 n=10)
All/c=wrapper_that_contains_aser-4                                     1.000 ± 0%     1.000 ± 0%         ~ (p=1.000 n=10) ¹
All/c=empty_joiner-4                                                   1.000 ± 0%     0.000 ± 0%  -100.00% (p=0.000 n=10)
All/c=joiner_that_contains_nil-4                                       1.000 ± 0%     0.000 ± 0%  -100.00% (p=0.000 n=10)
All/c=joiner_that_contains_nil_and_match-4                             1.000 ± 0%     0.000 ± 0%  -100.00% (p=0.000 n=10)
All/c=joiner_that_contains_non-nil_and_match-4                         1.000 ± 0%     0.000 ± 0%  -100.00% (p=0.000 n=10)
All/c=joiner_that_contains_match_and_non-nil-4                         1.000 ± 0%     0.000 ± 0%  -100.00% (p=0.000 n=10)
All/c=joiner_that_contains_two_matches-4                               1.000 ± 0%     0.000 ± 0%  -100.00% (p=0.000 n=10)
All/c=deeply_nested_joiner_that_contains_non-nil_and_three_matches-4   1.000 ± 0%     0.000 ± 0%  -100.00% (p=0.000 n=10)
All/c=mix_of_wrappers_and_joiners-4                                    1.000 ± 0%     0.000 ± 0%  -100.00% (p=0.000 n=10)
All/c=mix_of_wrappers_and_joiners_that_contains_asers-4                1.000 ± 0%     1.000 ± 0%         ~ (p=1.000 n=10) ¹
All/c=joiner_that_contains_many_false_asers-4                          1.000 ± 0%     1.000 ± 0%         ~ (p=1.000 n=10) ¹
geomean                                                                           ²               ?                       ² ³
¹ all samples are equal
² summaries must be >0 to compute geomean
³ ratios must be >0 to compute geomean

Comment From: jub0bs

@imax9000 Wrapping errors.As in GenericAsBikeshed2 will give you ergonomics and type safety, but implementing GenericAsBikeshed2 from scratch will also give you better performance.

Comment From: imax9000

@imax9000 Wrapping errors.As in GenericAsBikeshed2 will give you ergonomics and type safety, but implementing GenericAsBikeshed2 from scratch will also give you better performance.

That's true, but I'm more concerned with the interface. After all, implementation can be changed later on, if the need arises.

Comment From: mitar

How would a generic errors.As handle the case, that currently it is documented, that:

An error type might provide an As method so it can be treated as if it were a different error type.

Comment From: jub0bs

@mitar Can you clarify? Nothing in a generic implementation of errors.As would prevent that use case; in fact, my implementation's test suite covers that use case.

Comment From: mitar

I see. Awesome!

Comment From: neild

Summarizing my understanding of the discussion to date:

The errors.As function was added before we had support for type parameterized functions. The proposal is to add a type-parameterized version of As. Call this function AsA for the moment (more on naming below).

Arguments in favor

Ease of use

Compare:

var pe *os.PathError
if errors.As(err, &pe) {
  // ...
}
if pe, ok := errors.AsA[*os.PathError](err); ok {
  // ...
}

The AsA form can be written in one less line of code and allows the pe variable to be scoped to the conditional.

Compile-time type checking

errors.As's target parameter must be a pointer to an error value to fill in, but the type is defined as an any. We do have a vet check that validates As calls, but AsA would permit the compiler to catch errors.

// This is incorrect: PathError doesn't implement error. (*PathError does.)
// The errors.As call will panic, but the compiler cannot validate it.
var pe os.PathError
if errors.As(err, &pe) {
  // ...
}
// The wrong type here can be detected at compile time.
if pe, ok := errors.As[os.PathError](err); ok {
}

Performance

@jub0bs reports that AsA can be substantially faster than As: https://github.com/golang/go/issues/51945#issuecomment-2972892457

Arguments against

Some arguments from the pre-generics discussion that led up to As being added: - @rsc argued that a type-parameterized As wouldn't be as nice as the current version: https://github.com/golang/go/issues/29934#issuecomment-490091428 - @dsnet pointed out that a type-parameterized As can't be used in a switch statement: https://github.com/golang/go/issues/29934#issuecomment-460093518

There's also a general argument of the benefit not being worth the added churn; As works well enough.

Overall, discussion here seems to be in favor of AsA.

Naming

The original post for this issue proposes naming the function IsA. I argued that a generic version of As should continue to use the As name as a base. I think discussion has converged on AsA as the preferred name.

IsA?

@jalaziz (https://github.com/golang/go/issues/51945#issuecomment-2127941523) argues for adding two functions: One to convert an error to a type, and one to query whether it contains a value of that type.

func AsA[E error](err error) (E, bool)
func IsA[E error](err error) bool

In this case, the IsA implementation is just:

func IsA[E error](err error) bool {
  _, ok := AsA[E](err)
  return ok
}

Type of the constraint: error or any?

You can currently write the following code:

var x interface { Foo() }
if errors.As(err, &x) {
  // note that x does not implement error
}

If we define func AsA[E error](error) (E, bool), then we cannot write the equivalent:

// Incorrect: interface { Foo() } does not satisfy the type constraint (error).
if errors.As[interface { Foo() }](err) {
  //
}

However we can write errors.As[interface { error; Foo() }](err), so this doesn't seem like much of a limitation.

Target parameter or return the found value?

Should we define AsA as:

  1. func AsA[E error](err error) (_ E, ok bool)
  2. func AsA[E error](err error, target *E) bool

@jub0bs reports (https://github.com/golang/go/issues/51945#issuecomment-2391053096) that the first form (returning E) performs better. In addition, it seems to me that it's clearly the more natural form for the language--the only reason As takes a return parameter rather than returning a value is because we had no other way to specify the type it should look for.

Proposal

Based on the above, I propose:

package errors

// AsA finds the first error in err's tree that has the type E, and if one is found, returns that error value and true.
// Otherwise it returns the zero value of E and false.
func AsA[E error](err error) (_ E, ok bool)

Comment From: TapirLiu

Why does it return two results? Isn't one enough? Just return nil when fail to match?

Comment From: mateusz834

Why does it return two results? Isn't one enough? Just return nil when fail to match?

Because of:

type SomeError struct{}
AsA[SomeError](err)

Comment From: TapirLiu

SomeError doesn't satisfy error.

Comment From: TapirLiu

Er, okay, the return might be not an interface.

Comment From: jub0bs

@neild Thanks for reviving this proposal. ❤️

We do have a vet check that validates As calls, but AsA would permit the compiler to catch errors.

One vet check that would be useful for AsA is for instantiations of the form AsA[error], which, unless I'm missing something, are never useful.

[...] a type-parameterized As can't be used in a switch statement

That's true, but I find this point moot. Even with good ol' errors.As, if you want to use it within a switch statement, you need to declare the target somewhere and it cannot be within a case clause; it has to be above the switch statement or in the latter's optional simple statement.

a type-parameterized As wouldn't be as nice as the current version

Even if AsA becomes a reality, there's one infelicity that I haven't seen mentioned anywhere: the optional As(any) bool method feels incongruent with a generic AsA function, but the latter would have to cater for the former. I can't think of a way to improve things in that regard.

Naming

This is the most difficult part! In an ideal world, we could simply generify errors.As; we could actually do it thanks to the go directive, but I'm not sure it's worth it. At least, we can consider deprecating errors.As in favour of errors.AsA.

Edit: If we're inclined to stray from the original name, perhaps "ContainsSome" would be an apter name than As or AsA:

if pe, ok := errors.ContainsSome[*os.PathError](err); ok {
}

One minor problem with "AsA" is that it doesn't roll off the tongue when the name of the target type starts with a vowel (in which case it should ideally read as "AsAn").

Comment From: aclements

This proposal has been added to the active column of the proposals project and will now be reviewed at the weekly proposal review meetings. — aclements for the proposal review group

Comment From: eikemeier

I'm willing to donate fillmore-labs.com/exp/errors which has

func Has[T error](err error) (T, bool)

Added advantage: When you query for the wrong type (Pointer vs. Value) you still get a match, see the example. A strict version without this feature is also available.

Comment From: jub0bs

@eikemeier If my benchmark results are to be believed, my implementation beats your Has function in terms of performance. Try it out for yourself by patching github.com/jub0bs/errutil with the following and running the benchmarks:

diff --git a/errutil_test.go b/errutil_test.go
index 0d1ed06..d4f63ec 100644
--- a/errutil_test.go
+++ b/errutil_test.go
@@ -9,6 +9,7 @@ import (
        "slices"
        "testing"

+       errors2 "fillmore-labs.com/exp/errors"
        "github.com/jub0bs/errutil"
 )

@@ -208,6 +209,13 @@ func BenchmarkAll(b *testing.B) {
                        }
                }
                b.Run("impl=errutil.Find/c="+bc.desc, f)
+               f = func(b *testing.B) {
+                       b.ReportAllocs()
+                       for range b.N {
+                               simpleErrorSink, boolSink = errors2.Has[simpleError](bc.err)
+                       }
+               }
+               b.Run("impl=errors2.Has/c="+bc.desc, f)
        }
 }

Similar observation about your HasError function.

Added advantage: When you query for the wrong type (Pointer vs. Value) you still get a match, see the example. A strict version without this feature is also available.

I have to think about it some more, but I'm not sure whether such flexibility should be considered an advantage. 🤔

Comment From: eikemeier

@eikemeier If my benchmark results are to be believed, my implementation beats your Has function in terms of performance.

This is expected, since Has checks for other error forms too. The question if it would make a significant difference in practice, given that you might have fewer bugs.

Similar observation about your HasError function.

I didn't tune the implementation for performance. I'm more interested in the outcome of the discussion, and for that this feasibility study is sufficient.

Added advantage: When you query for the wrong type (Pointer vs. Value) you still get a match, see the example. A strict version without this feature is also available. I have to think about it some more, but I'm not sure whether such flexibility should be considered an advantage. 🤔

As mentioned: I'm more interested in participating in the discussion than winning benchmarks. With this implementation we could discuss the benefits on concrete examples, when we want to have it we can tune it for performance, when we don't we have learned something without spending time on tuning.

Generally I would think benchmarking should be done on real-world data, which I didn't compile for this experiment.

Also, again: I don't think what we have is too slow, I'm more concerned about ergonomy, readability and correctness. When I want to have something fast, I can put it into my code - nothing in the standard library needed. I want to centralize that to make it reachable to linters - errortype in my case ;)

Otherwise I really like that others are interested in this issue.

Comment From: Merovius

@neild Minor point, but:

The AsA form can be written in one less line of code and allows the pe variable to be scoped to the conditional.

So can the As form. That's how I usually use As.

if pe := new(os.PathError); errors.As(err, &pe) {
    // use pe
}

Comment From: znkr

@Merovius, IIUC, thenew here is redundant. You could instead use

if pe := (*os.PathError)(nil); errors.As(err, &pe) {
    // use pe
}

This is all very subtle, which I interpret as another reason for AsA.

Comment From: eikemeier

@neild Minor point, but:

The AsA form can be written in one less line of code and allows the pe variable to be scoped to the conditional.

So can the As form. That's how I usually use As.

if pe := new(os.PathError); errors.As(err, &pe) { // use pe }

I for my part find it hard to recognize that you are looking for an *os.PathError here. My linter will, and it is correct code, but easy to misread in a code review IMHO. Also

  if pe := new(*os.PathError); errors.As(err, pe) {
    // use *pe
  }

is bad because of different reasons.

@Merovius, IIUC, thenew here is redundant. You could instead use

if pe := (*os.PathError)(nil); errors.As(err, &pe) { // use pe } This is all very subtle, which I interpret as another reason for AsA.

I have an extra section in my blog entry why I think this is hard to read. I'm very used to:

  var pe *os.PathError
  if errors.As(err, &pe) {
    // use pe
  }

which already splits the error looked for and handling over different lines, but everything else makes me think. Which, considered that error handling isn't usually the main task handled, is error prone.

Comment From: TapirLiu

In my impression, the current errors.As function is the only function which uses an input parameter for output in the std lib. The pattern is used popular in C, but rarely in Go. The fact makes it look some weird. If the weirdness is acceptable, then the AsA function can be designed as:

func AsA[E error](err error) *E

which returns nil for unmatched.

Comment From: Merovius

In my impression, the current errors.As function is the only function which uses an input parameter for output in the std lib.

encoding/json.Unmarshal. Also, pretty much anything that has to work with arbitrary types and existed pre-generics.

Comment From: TapirLiu

More precisely, I mean it often need to pass **T to errors.As.

The AsA proposal (https://github.com/golang/go/issues/51945#issuecomment-3208246400) has a disadvantage over the current errors.As.

func AsA[E error](err error) (_ E, ok bool)

Some packages might expose some error values which types are unexported. Such unexported error types can't be used in AsA calls. But such cases should be rare.

And sometimes, even if an error type is exported, but it is verbose to write out it (needs an import etc), but we happen to get a value of it already.

It would be great if Go support a TypeOf alike builtin.

Comment From: jub0bs

@TapirLiu

Some packages might expose some error values which types are unexported.

Wouldn't this be an indication of a design mistake (similarly to exported functions with results of non-exported types), though?

Comment From: Merovius

Some packages might expose some error values which types are unexported. Such unexported error types can't be used in AsA calls. But such cases should be rare.

To get this right: the point is that someone might do

package foo
var ErrSomething = &unexportedErrorType{…}

package bar
func F() {
    var err error // from somewhere
    if errors.As(err, &foo.ErrSomething) { … }
}

Or perhaps

package foo
func G() *unexportedErrorType { … }

package bar
func F() {
    var err error
    x := foo.G()
    if errors.As(err, &x) { … }
}

I can't really think of any other way to use errors.As with unexported errors from a different package.

I'd be okay making either of these impossible (I'd probably call that a feature, not a bug). Though FWIW if you really want to, you can still use either with AsA:

package bar

func as[T error](_ T, target error) (T, bool) {
    return errors.AsA[T](target)
}

func F() {
    var err error
    if x, ok := as(foo.ErrSomething, err); ok { … }
    if y, ok := as(foo.G(), err); ok { … }
}

Comment From: TapirLiu

@Merovius Except the type parameter type is the target error type, the idea has been implemented in nstd.TrackErrorOf: https://github.com/go101/nstd/commit/14ade6415128029d5841a74416d121475e98b2a4

@jub0bs

Some packages might expose some error values which types are unexported.

Wouldn't this be an indication of a design mistake (similarly to exported functions with results of non-exported types), though?

Not always in my opinion.

Comment From: mgnsk

We've been using such a function for some time now:

// Extract finds the first error in err's tree that matches T, and if one is found, returns the error value.
func Extract[T error](err error) (T, bool)

This design allows use of a pointer to a value error which still implements error but if Extract implementation is naive (such as wrapping errors.As), there is a silent runtime failure which we prevented by implementing an analyzer rule.

type MyError struct{}

func (e MyError) Error() string { return "" }

func example() {
    var err error
    // This compiles but silently does not match at runtime.
    // An analyzer catches this case.
    Extract[*MyError](err)
}

Comment From: TapirLiu

The compile-time ability of Go generics is too weak. If there is a BaseType builtin, then this problem can be solved.

[edit]: but Go's runtime reflection is powerful enough to solve this problem.

Comment From: eikemeier

@mgnsk

We've been using such a function for some time now:

// Extract finds the first error in err's tree that matches T, and if one is found, returns the error value. func ExtractT error (T, bool) This design allows use of a pointer to a value error which still implements error but if Extract implementation is naive (such as wrapping errors.As), there is a silent runtime failure which we prevented by implementing an analyzer rule.

Is this similar to fillmore-labs.com/exp/errors combined with. the linter or some other approach? Is the source available?

Comment From: mgnsk

@mgnsk

We've been using such a function for some time now: // Extract finds the first error in err's tree that matches T, and if one is found, returns the error value. func ExtractT error (T, bool) This design allows use of a pointer to a value error which still implements error but if Extract implementation is naive (such as wrapping errors.As), there is a silent runtime failure which we prevented by implementing an analyzer rule.

Is this similar to fillmore-labs.com/exp/errors combined with. the linter or some other approach? Is the source available?

Our implementation is exactly like the IsA function in the first post (just wraps errors.As). Can't share the exact code right now but the linter contains something like

var errInterface = types.Universe.Lookup("error").Type().Underlying().(*types.Interface)

if p, ok := typeParam.(*types.Pointer); ok && types.Implements(p.Elem(), errInterface) {
    // report use of value error as pointer type parameter

and a pseudo-snippet of its test suite showing that invalid use of pointer error does not compile but invalid use of value error does compile.

func (e ValueError) Error() string

func (e *PointerError) Error() string

// Correct usage of value error.
Extract[ValueError](err)

// Invalid usage of value error as pointer.
Extract[*ValueError](err) // want `Extract: use of value error as pointer type parameter`

// Correct usage of pointer error.
Extract[*PointerError](err)

// Invalid usage of pointer error as value.
// Does not compile:
// Extract[PointerError](err)

Comment From: TapirLiu

So the question is, if a value (target) of value MyError type is in the tree of anErr, should errors.As(anErr, new(*MyErr0r)) return true or false? The current implementation returns false. My current opinion is it should return true.

Comment From: jub0bs

@TapirLiu Regardless of the merit of such a change in behaviour of errors.As, I'm not sure that this proposal is the best place to discuss it. 🤷

Comment From: TapirLiu

The issuse is for AsA alikes, which behaviors have not been decided yet. So I think it is okay to discuss it here.

Comment From: rednafi

The readability of the proposal is worse than what we have today. Saving a few lines is not worth taking the hit there imo.

Sure, this is an add on but not sure if it's worth increasing the API surface area here.

Comment From: jub0bs

@rednafi Increasing the API surface is a valid concern (although we could mitigate that by deprecating errors.As); but the signature of the proposed function would allow better performance, ergonomics, and type safety. Surely, that counts for something.

Comment From: hu3bi

  • No runtime panic possibility

  • Compile time type safety

Those upsides are really huge!

Comment From: eikemeier

@TapirLiu Regardless of the merit of such a change in behaviour of errors.As, I'm not sure that this proposal is the best place to discuss it. 🤷

Do you think it should be discussed?

I tend to think that this is related enough to be discussed here. I'm fine with both outcomes (we want it or we reject it), but I'm very interested in the discussion.

  • No runtime panic possibility
  • Compile time type safety

Those upsides are really huge!

I'm not so sure: There is already the errorsas Analyzer, so runtime panics should be rare. What kind of “compile time type safety” are you thinking of, especially when you query for the wrong error type?

Comment From: hu3bi

@eikemeier

No runtime panic possibility Compile time type safety

Those upsides are really huge!

I'm not so sure: There is already the errorsas Analyzer, so runtime panics should be rare. What kind of “compile time type safety” are you thinking of, especially when you query for the wrong error type?

This code compiles: https://go.dev/play/p/sSDO8hTsljQ With a generic function, it would be checked at compile time and wouldn't compile at all.

I agree, that the errorsas analyzer, is nice and solves this problem partially. But the analyzer could be retired if we used a generic function. It also doesn't fully eliminate runtime panics.

So somebody might compile a binary inside CI and deploy it.


(No one in their right mind wouldn't use linters or at least go vet in their ci before compiling.)

Comment From: aclements

While it's unfortunate to have duplication in the API, it's also natural as Go's capabilities change over time. Given the static type safety and performance of the proposed API, we've decided to move forward with this.

Comment From: aclements

Based on the discussion above, this proposal seems like a likely accept. — aclements for the proposal review group

The proposal details are in https://github.com/golang/go/issues/51945#issuecomment-3208246400

Comment From: jub0bs

@aclements That's great news!

Just a reminder that github.com/jub0bs/errutil contains an implementation of the proposed function (albeit under a different name) along with a test/benchmark suite inspired by errors.As's; at the very least, it should be a good starting point for whoever eventually picks this up.

(I'm looking forward to archiving that repo once the new function is added to the standard library!)

Comment From: coxley

reflect has added type-aware helpers in the last few releases: - reflect.TypeFor - reflect.TypeAssert

Those names feel more natural for their use than errors.AsA. Throwing out a couple as a hail mary :)

  • errors.Cast or errors.Assert
    • I like these names, but they semantically sound like their argument should be of the target vs. unwrapping to find the target.
  • errors.AsFor
    • Rolls of the tongue a bit more awkwardly, but maybe equal to errors.AsA?
  • errors.Find
    • This feels the most appropriate, but @neild's summary says we want to stay as close to errors.As as possible.

Comment From: mvdan

errors.AsT or errors.AsType? I actually think either of those would help with the current confusion for new users between As and Is.

Comment From: apparentlymart

While I must admit I would not have thought of it myself, I quite like @coxley's errors.Find now it's on the table:

Using a verb like this hints that this is going to do some more work than just a type conversion (searching a potentially-deep tree of Unwrap() error and Unwrap []error results), rather than relying only on the documentation for that.

I do of course concede that it makes it very unlike errors.As, but part of me thinks that errors.Find might've been a better name for As in the first place, given what it actually does, and so I feel okay with encouraging new developers to use errors.Find and leaving errors.As behind as a legacy thing used only by older code.

Not a strong opinion, though. I would also be happy to stick with errors.AsA. 🤷‍♂️ (If the only remaining concern is what to name it then I think that's a great sign!)

Comment From: jub0bs

I quite like @coxley's errors.Find now it's on the table [...]

FWIW, "Find" is the name I went for in my experimental package. It's not perfect, but I do prefer it to "AsA", if only because it sidesteps the pronunciation problem of a target type whose qualified identifier starts with a vowel (in which case "AsAn" would be better).

I do of course concede that it makes it very unlike errors.As [...]

I'm not convinced that "As" and "Is" were apt function names to begin with; I do think errors.As's name plays a part in the confusion that, as @mvdan points out, many Gophers experience.

Comment From: coxley

@jub0bs Whoops, I hadn't realized that! It's a good name.

I'm not convinced that "As" and "Is" were apt function names to begin with; I do think errors.As's name plays a part in the confusion that, as @mvdan points out, many Gophers experience.

After using it for a couple years, it does feel like Find and Has would have been clearer in what they do.

Comment From: hu3bi

I'm not convinced that "As" and "Is" were apt function names to begin with; I do think errors.As's name plays a part in the confusion that, as @mvdan points out, many Gophers experience.

The main use case for errors.As is to find out whether some error is of a specific type. The 'As' only makes sense because the target error gets assigned if a match occurred. The target error can then be used to extract more information from that error.

Maybe the function is just doing too much for what the name suggests.

Comment From: zzhaolei

How about using errors.Match?

Comment From: jub0bs

@zzhaolei Maybe it's just me, but I would expect a function named "Match" to only return a boolean (and perhaps an error). See for instance https://pkg.go.dev/path#Match.

Comment From: apparentlymart

I am not actually proposing this here because it's separate from what we've already been discussing, but a possible later addition would be a variant that yields all errors matching a given type, for situations where the same error type could appear multiple times in an errors.Join, or similar.

For example:

package errors

// FindAll searches the tree of errors beneath
// error for any that match type T, yielding
// them in an unspecified order.
//
// If no errors in the tree match T then the
// sequence is empty.
func FindAll[T error](err error) iter.Seq[T]

I mention this here now only because I think the name FindAll pairs nicely with Find representing "find one". There isn't such a clear name pairing for some of the other names we've discussed, including the original AsA.

I don't want to derail the current proposal talking about this secondary proposal. The question I'm intending to ask is only: do we want to choose a name that would leave room for something like this to be added in a later proposal, or is this idea unlikely enough that it should not influence our current discussion at all?

(If we can manage to communicate with emoji reactions first here then maybe that will minimize disruption to existing discussion: :thumbsdown: for "we should not try to leave room for something like this", :thumbsup: for "we should try to leave room for something like this to be proposed in future". I expect this is only worth further discussion if the emoji voting is provocative.)

Comment From: earthboundkid

FindAll is conceptually the same as #66455, which was declined at the time, but which I've since had need of. See https://github.com/earthboundkid/resperr/blob/master/chain.go

The decline was soft:

I said hold last time but I think we should probably just decline this and we can always file a new one with more evidence later if it is gathered.

Comment From: earthboundkid

I think it's a little unfortunate to add Find just because of performance problems with As. (The API awkwardness/vet check issues don't strike me as that important.) If it's paired with FindAll, I'm less worried because it's at least covering new capabilities.

Comment From: mitar

Hm, that maybe this is not a bad idea, @earthboundkid. Maybe we need only FindAll which returns an iterator. Then one can do the following, if you want only the first error. No need for ok check.

for myErr := range error.FindAll[*MyCustomError](err) {
    // handle myErr
    break
}

Comment From: tianon

If we only add FindAll then the basic use case of doing this in an if (when you know there's only one or only care about one) is harder, which is the primary target of this proposal.

(Personally I like the naming and idea of both Find and FindAll)

Comment From: jub0bs

```go package errors

// FindAll searches the tree of errors beneath // error for any that match type T, yielding // them in an unspecified order. // // If no errors in the tree match T then the // sequence is empty. func FindAllT error iter.Seq[T] ```

I don't want to stray too much from this proposal, but wouldn't a function returning an iterator over the entire error tree be more useful/flexible than the proposed FindAll function?

func All(err error) iter.Seq[error]

For example, you may well want to search the tree for errors of different concrete types via a single tree traversal; that wouldn't be possible with FindAll (unless the types of interest satisfy a common interface, I guess), but that would be possible with All.

Comment From: earthboundkid

If we add errors.All, then we really are just reopening https://github.com/golang/go/issues/66455.

One of the objections to All there was that it doesn't have a lot of uses and you can build it yourself if you need it. To me, the main use for All would be some kind of logging system where you want to record the entire error tree (think Sentry or Honeycomb). I don't think downstream users would really need it. But FindAll seems more useful because you might want to look at some concrete error and decide what version of it takes priority.

Comment From: apparentlymart

I guess the above discussion is in a sense implying a naming convention where "find" means "search by error type", "all" means "return multiple results", and so therefore "FindAll" is the combination of both where it finds potentially multiple results by error type.

Unless someone wants to directly propose dropping this proposal for now in favor of one of the others, perhaps a way to pose this question in the same vein as my earlier comment would be: is there consensus that this is a reasonable naming convention to follow?

If the naming convention has consensus then the other two variants could each go through their own proposal process (mostly) independently of this one.

Comment From: neild

I'm not convinced that we're going to reduce confusion by introducing multiple names for the same thing.

The relationship between errors.As and errors.AsA is fairly obvious: They're the same thing, but AsA is type-parameterized. The relationship between errors.As and errors.Find is not obvious at all; they sound like they do different things.

Maybe that's a change we should make, but it feels like a substantial amount of scope creep from just introducing a type-parameterized version of As.

Comment From: Merovius

@jub0bs

unless the types of interest satisfy a common interface, I guess

You mean like error?

Comment From: apparentlymart

FWIW I was understanding the effect of this proposal as the new function effectively replacing the old one, such that errors.As would be to the new function as interface{} is to any: an old way of writing something that we don't use in new code, and that over time the various "modernization" linters might even start recommending to change.

Under that framing I would prefer to choose a name that makes sense in its own right rather than naming it in relation to the old thing we want to move away from. However, I understand that I may have misunderstood the goal; if we expect that the existing errors.As should continue to be used alongside the new function indefinitely then I would understand the argument that the new function should be named in relation to errors.As.

(And as previously mentioned, I don't feel strongly about this naming question anyway; my earlier comments were only to identify reasons why we might prefer one naming convention over another. I've no objection to errors.AsA if there's consensus that it's a good fit for the goals of this proposal and, more broadly, for the likely future evolution of package errors.)

Comment From: jub0bs

@Merovius I sense sarcasm, but you're right: that would work. However, it's unclear to me whether performance would suffer (due to all those useless type assertions to error).

Comment From: Merovius

You could always fast-path by if _, ok := any(new(T)).(*error); ok { listAll() }

Comment From: apparentlymart

Even if it isn't true today, it seems to me that a compiler optimization could trivially notice the redundancy in type-asserting a variable that's statically-typed as error using .(error) and optimize away the dynamic type-check. Even if the code generator were trying to reuse the same generated code across all T that are interface types, constraining that type parameter as T error should provide all the information needed to conclude that the type assertion is completely redundant.

With that said: I think this detail is only relevant to the current discussion if we intend to replace this proposal with FindAll. If this proposal continues to be about AsA/Find then I suggest we save discussion about whether we need both Find and FindAll for a separate proposal, or for a reopening of https://github.com/golang/go/issues/66455. I know I wasn't proposing to discard AsA/Find, though of course I can't speak to what others are intending. 😀

Comment From: anonld

Although I have not been closely following this proposal, it seems to me that (correct me if I am wrong) initially there is a recurring theme about checking the target error type is pointer or value. If I observe correctly, the receivers of Error() string methods of user-defined error types are far more often pointers than values, so I wonder whether the permissive type constraint in the function signature is a bug or feature. Could anyone help evaluate stricter alternative (e.g. func AsA[E errptr[T], T any](err error) (target E, ok bool))? It would be even better if the evaluation is supported with real-world corpus or counterexample.

The following is the definition of errptr[T] (its name and whether to export it may need further discussion):

type errptr[T any] interface {
    *T
    error
}

This stricter AsA alternative is tested on playground. (https://go.dev/play/p/yoQnlWES7qk) Note: 1. Please be aware that this tested implementation is a bit messy, and the testing code is borrowed from stdlib. 2. The Inspect function in the link, which refactored the error tree traversal logic, is considered as implementation detail and I am not sure whether this Inspect function should be exported. 3. There is no benchmark for this alternative AsA implementation, and the performance implication of refactoring is uncertain. 4. In order to avoid scope creep, would any moderator or member from the proposal review team help clarify whether the accompanying Inspect function is still within the scope of this proposal discussion? If not, would any interested party help spin it off (since I may not have enough bandwidth for follow-up discussion)? 5. Similarly, the scope check and potential spin-off apply to this generic version of errors.Is (func errorsIs[T interface { comparable; error }](err error, target T) bool), which imposes stricter type constraints for the target error so as to check misuse at compile time and avoid the overhead of reflective programming (at the cost of increased API surface). 6. Sorry for interrupting the current discussion about type assertion and Find/FindAll. Hopefully, my participation would not make the discussion more chaotic. It would be even better if someone may help manage this kind of topic divergence.


  1. Performance arguments often need to argue that there isn't some way that the compiler's optimizer couldn't get smarter about optimizing the current form too.

    It seems like optimizing the dynamic form to be roughly equivalent to the generic form is possible in principle giving a suitably effective inliner and devirtualizer, but that seems like a much bigger lift than a generic form of errors.As, and my GenericAsBikeshed1 example still comes with the advantage of statically checking that what's provided in target is a pointer, whereas that's a runtime error in the current errors.As