Skip to content

fillmore-labs.com/exp/errors is an experimental Go library providing two enhanced, generic alternatives to errors.As for inspecting error chains with improved ergonomics and type safety.

License

Notifications You must be signed in to change notification settings

fillmore-labs/errors-exp

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Errors

Go Reference Test CodeQL Coverage Go Report Card License

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.

Motivation

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.

Function Overview

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 - The HasError API plus pointer-value mismatch handling.

  • AsError - The classic errors.As API with generic type safety.

  • As - The classic errors.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 - Enhanced Ergonomics

HasError Overview

HasError is a generic, type-safe replacement for errors.As that offers improved ergonomics and readability.

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

HasError Key Benefits

No Target Variable Needed

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 */ }

Improved Readability

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 { /* ... */ }

Interface Support

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 */ }

When to Use HasError

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 - Pointer-Value Flexibility

Has Overview

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 Key Benefits

Automatic Pointer-Value Matching

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! */ }

Prevents Common Bugs

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)
  }

When to Use Has

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.

Limitations

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 */ }

Classic API with Enhanced Safety

If you prefer the traditional errors.As API that uses target variables, this library provides enhanced versions that add the same benefits:

AsError - Type-Safe errors.As

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 - Type-Safe with Flexible Matching

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.

Migration Guide

From errors.As to HasError

  // 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) }

From HasError to Has

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 { /* ... */ }

Further Reading

License

This project is licensed under the Apache License 2.0. See the LICENSE file for details.

About

fillmore-labs.com/exp/errors is an experimental Go library providing two enhanced, generic alternatives to errors.As for inspecting error chains with improved ergonomics and type safety.

Topics

Resources

License

Stars

Watchers

Forks

Languages