fillmore-labs.com/exp/errors
is an experimental Go library that provides four enhanced, generic alternatives to
errors.As
for inspecting error chains with improved ergonomics and type safety.
In Go, error types can be designed for use as either values or pointers. When inspecting an error chain, a mismatch
between the type you're looking for (e.g., MyError
) and the type that was actually wrapped (e.g., *MyError
) can lead
to subtle, hard-to-find bugs where errors are missed.
See this blog post to read more about the issue and the errortype
linter.
This library provides four functions in two complementary pairs, each building on Go's standard errors.As
:
-
HasError
- Ergonomic and type-safe, returns the found error. -
Has
- TheHasError
API plus pointer-value mismatch handling. -
AsError
- The classicerrors.As
API with generic type safety. -
As
- The classicerrors.As
API plus pointer-value mismatch handling.
Feature | errors.As |
HasError |
Has |
AsError |
As |
---|---|---|---|---|---|
No target variable needed | ❌ | ✅ | ✅ | ❌ | ❌ |
Pointer-value mismatch handling | ❌ | ❌ | ✅ | ❌ | ✅ |
Type safe generics | ❌ | ✅ | ✅ | ✅ | ✅ |
HasError
is a generic, type-safe replacement for errors.As
that offers improved ergonomics and readability.
func HasError[T error](err error) (T, bool)
With errors.As
, you must declare a variable beforehand:
var myErr *MyError
if errors.As(err, &myErr) { /* ... use myErr */ }
HasError
allows you to declare and check in one line:
if myErr, ok := HasError[*MyError](err); ok { /* ... use myErr */ }
The syntax for errors.As
can sometimes obscure intent, especially when you only need to check for an error's presence
without using its value. Note how errors.As
requires a pointer to a struct literal to check for a
value type:
// This check is valid, but not immediately clear.
if errors.As(err, &x509.UnknownAuthorityError{}) { /* ... */ }
HasError
makes the intent explicit and is easier to read:
// The type is clearly specified as a generic parameter.
if _, ok := HasError[x509.UnknownAuthorityError](err); ok { /* ... */ }
HasError
works seamlessly with interfaces. To check if an error implements a specific interface:
// Single-line variant
if e, ok := HasError[interface { error; Temporary() bool }](err); ok && e.Temporary() { /* handle temporary error */ }
// Or, using a named interface:
type temporary interface { error; Temporary() bool }
if e, ok := HasError[temporary](err); ok && e.Temporary() { /* handle temporary error */ }
Choose HasError
when you need:
- Improved ergonomics compared to
errors.As
. - Strict type matching, where pointers and values are treated as distinct types.
- To check if an error in the chain implements a specific interface.
Has
provides all the benefits of HasError
plus automatic handling of pointer-value type mismatches, preventing
subtle bugs.
func Has[T error](err error) (T, bool)
Has
automatically resolves pointer-value mismatches, finding errors that errors.As
would miss:
// Scenario 1: Looking for a value (MyError), but a pointer (*MyError) was wrapped.
err := &MyError{msg: "oops"}
if myErr, ok := Has[MyError](err); ok { /* This matches! */ }
// Scenario 2: Looking for a pointer (*MyError), but a value (MyError) was wrapped.
err2 := MyError{msg: "oops"}
if myErr, ok := Has[*MyError](err2); ok { /* This also matches! */ }
This mismatch would silently fail with errors.As
. In the example below, aes.NewCipher
returns an aes.KeySizeError
value, but the check incorrectly looks for a pointer (*aes.KeySizeError
):
key := []byte("My kung fu is better than yours")
_, err := aes.NewCipher(key)
// With errors.As, this check for a pointer type fails because the
// actual error is a value.
var kse *aes.KeySizeError
if errors.As(err, &kse) {
// This code is never reached.
}
// With Has, the check succeeds because it handles the pointer-value mismatch.
if kse, ok := Has[*aes.KeySizeError](err); ok {
fmt.Printf("Invalid AES key size: %d bytes. Key must be 16, 24, or 32 bytes.\n", *kse)
}
Choose Has
when you want:
- The most robust error detection, with all the ergonomic benefits of
HasError
. - Automatic handling of pointer-value mismatches to prevent subtle bugs caused by inconsistent error wrapping.
Unlike errors.As
, interface types provided to Has
or HasError
must embed the error
interface.
// This is valid with errors.As:
var temp interface{ Temporary() bool }
if errors.As(err, &temp) && temp.Temporary() { /* handle temporary error */ }
// With Has or HasError, the interface must embed `error`:
if temp, ok := Has[interface { error; Temporary() bool }](err); ok && temp.Temporary() { /* handle temporary error */ }
If you prefer the traditional errors.As
API that uses target variables, this library provides enhanced versions that
add the same benefits:
AsError
provides generic type safety to prevent target variable type mismatches:
func AsError[T error](err error, target *T) bool
// Usage
var myErr *MyError
if AsError(err, &myErr) {
// myErr is guaranteed to be the correct type
// No risk of type assertion bugs
}
As
combines type safety with the same pointer-value mismatch handling as Has
:
func As[T error](err error, target *T) bool
// Usage
var myErr *MyError
if As(err, &myErr) {
// Also handles pointer-value mismatches automatically
// Most robust option with familiar API
}
Both functions prevent common type-related bugs while maintaining the familiar errors.As
API that some developers
prefer.
// Before
var myErr *MyError
if errors.As(err, &myErr) { return fmt.Errorf("unexpected MyError: %w", myErr) }
// After
if myErr, ok := HasError[*MyError](err); ok { return fmt.Errorf("unexpected MyError: %w", myErr) }
If you suspect pointer-value mismatches are causing issues, replace HasError
with Has
.
// If this check sometimes fails unexpectedly:
if myErr, ok := HasError[MyError](err); ok { /* ... */ }
// Switch to Has for more robust matching:
if myErr, ok := Has[MyError](err); ok { /* ... */ }
- Blog: Understanding Go Error Types: Pointer vs. Value - Background on pointer-value type mismatches
- Go proposal: errors: As with type parameters - Community discussion on improving
errors.As
with generics
This project is licensed under the Apache License 2.0. See the LICENSE file for details.