Go Proposal: Type-Safe Error Checking with errors.AsType

Go Programming

Discover Go 1.26's new `errors.AsType` function, a generic, type-safe, and faster alternative to `errors.As` for robust error handling.

The Go team has accepted a significant proposal introducing errors.AsType, a modern, type-safe alternative to the existing errors.As function for error checking. This is part of a series explaining upcoming Go changes in simple terms, impacting Go version 1.26 with high importance for the standard library.

Summary

The new errors.AsType function provides a generic version of errors.As:

// go 1.13+
func As(err error, target any) bool

// go 1.26+
func AsType[E error](err error) (E, bool)

It offers a type-safe, faster, and more convenient way to handle errors:

Using errors.As (Go 1.13+):

var appErr AppError
if errors.As(err, &appErr) {
    fmt.Println("Got an AppError:", appErr)
}

Using errors.AsType (Go 1.26+):

if appErr, ok := errors.AsType[AppError](err); ok {
    fmt.Println("Got an AppError:", appErr)
}

While errors.As is not deprecated yet, errors.AsType is the recommended approach for new code due to its numerous advantages.

Motivation

The errors.As function requires developers to declare a variable of the target error type and pass a pointer to it, which can make the code verbose:

var appErr AppError
if errors.As(err, &appErr) {
    fmt.Println("Got an AppError:", appErr)
}

This verbosity becomes even more apparent when checking for multiple error types:

var connErr *net.OpError
var dnsErr *net.DNSError
if errors.As(err, &connErr) {
    fmt.Println("Network operation failed:", connErr.Op)
} else if errors.As(err, &dnsErr) {
    fmt.Println("DNS resolution failed:", dnsErr.Name)
} else {
    fmt.Println("Unknown error")
}

With the generic errors.AsType, you can specify the error type directly within the function call. This leads to shorter code and allows error variables to be conveniently scoped within their if blocks:

if connErr, ok := errors.AsType[*net.OpError](err); ok {
    fmt.Println("Network operation failed:", connErr.Op)
} else if dnsErr, ok := errors.AsType[*net.DNSError](err); ok {
    fmt.Println("DNS resolution failed:", dnsErr.Name)
} else {
    fmt.Println("Unknown error")
}

Another drawback of errors.As is its reliance on reflection, which can potentially lead to runtime panics if used incorrectly (e.g., passing a non-pointer or a type that doesn't implement error). While static analysis tools often mitigate these risks, errors.AsType offers superior benefits:

  • No runtime panics (compile-time type safety).
  • Fewer allocations.
  • Faster execution.
  • Compile-time type safety.

Ultimately, errors.AsType is designed to handle everything errors.As does, making it a drop-in improvement for new code.

Description

The AsType function will be added to the errors package with the following signature and behavior:

// AsType finds the first error in err's tree that matches the type E,
// and if one is found, returns that error value and true. Otherwise, it
// returns the zero value of E and false.
//
// The tree consists of err itself, followed by the errors obtained by
// repeatedly calling its Unwrap() error or Unwrap() []error method.
// When err wraps multiple errors, AsType examines err followed by a
// depth-first traversal of its children.
//
// An error err matches the type E if the type assertion err.(E) holds,
// or if the error has a method As(any) bool such that err.As(target)
// returns true when target is a non-nil *E. In the latter case, the As
// method is responsible for setting target.
func AsType[E error](err error) (E, bool)

The Go team also recommends using AsType over As for most scenarios. The documentation for errors.As will be updated to reflect this:

// As finds the first error in err's tree that matches target, and if one
// is found, sets target to that error value and returns true. Otherwise,
// it returns false.
// ...
// For most uses, prefer [AsType]. As is equivalent to [AsType] but sets its
// target argument rather than returning the matching error and doesn't require
// its target argument to implement error.
// ...
func As(err error, target any) bool

Example

Here's an example of opening a file and checking for a fs.PathError, comparing errors.As with errors.AsType:

Go 1.25 (using errors.As):

var pathError *fs.PathError
if _, err := os.Open("non-existing"); err != nil {
    if errors.As(err, &pathError) {
        fmt.Println("Failed at path:", pathError.Path)
    } else {
        fmt.Println(err)
    }
}
// Output: Failed at path: non-existing

Go 1.26 (using errors.AsType):

if _, err := os.Open("non-existing"); err != nil {
    if pathError, ok := errors.AsType[*fs.PathError](err); ok {
        fmt.Println("Failed at path:", pathError.Path)
    } else {
        fmt.Println(err)
    }
}
// Output: Failed at path: non-existing

Further Reading

It's important to note that while errors.AsType does not use the reflect package, it still relies on type assertions and interface checks. These operations access runtime type metadata, meaning AsType is not entirely "reflection-free" in the strictest sense.