errors

package module
v0.0.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 21, 2025 License: Apache-2.0 Imports: 2 Imported by: 0

README

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.

Documentation

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func As added in v0.0.3

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

As finds the first error in `err`'s tree that has type `T`, and if one is found, sets target to that error value and returns true. Otherwise, it returns 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, `As` examines `err` followed by a depth-first traversal of its children.

An error has the type `T` if the error's value is assignable to `T`, including cases where there are pointer-value mismatches (e.g., if `T` is `*MyError` but the found error is `MyError`, or vice versa), or if the error has a method `As(any) bool` that returns `true`. To accommodate pointer-value mismatches in `As` implementations, `As` tries different variations of the target type. In the latter case, the `As` method is responsible for setting `target`.

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

As panics if `target` is a nil pointer.

func AsError added in v0.0.3

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

AsError finds the first error in `err`'s tree that is of type `T`. If a matching error is found, sets target to that error value and returns `true`. Otherwise, it returns false.

This function provides a generic, type-safe alternative to the standard library's `errors.As`.

The error tree is traversed depth-first, starting with `err` itself. The tree is explored by repeatedly calling `Unwrap() error` or `Unwrap() []error`.

An error is considered to be of type `T` if:

  • The error's concrete value is assignable to `T`.
  • The error has a method `As(any) bool`, and calling `As` with `target` returns `true`. In this case, the `As` method is responsible for setting the value of `target`.

AsError panics if `target` is a nil pointer.

func DepthFirstErrorTree

func DepthFirstErrorTree(root error) iter.Seq[error]

DepthFirstErrorTree traverses an error tree depth-first and returns a sequence of errors starting from the root error. It supports both single error unwrapping (`Unwrap() error`) and multi-error unwrapping (`Unwrap() []error`) mechanisms. Nil errors or nil results from unwrapping are skipped during traversal.

func Has

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

Has finds the first error in `err`'s tree that has type `T`, and if one is found, returns that error and true. Otherwise, it returns the zero value for `T` (`nil` for pointer types) 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, `Has` examines `err` followed by a depth-first traversal of its children.

An error has the type `T` if the error's value is assignable to `T`, including cases where there are pointer-value mismatches (e.g., if `T` is `*MyError` but the found error is `MyError`, or vice versa), or if the error has a method `As(any) bool` that returns `true`. To accommodate pointer-value mismatches in `As` implementations, `Has` tries different variations of the target type. In the latter case, the `As` method is responsible for the result.

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

Example
package main

import (
	"crypto/aes"
	"errors"
	"fmt"

	errorx "fillmore-labs.com/exp/errors"
)

func main() {
	key := []byte("My kung fu is better than yours")
	_, err := aes.NewCipher(key)

	// With errors.As - this check fails silently.
	var target *aes.KeySizeError
	if errors.As(err, &target) {
		fmt.Printf("Wrong AES key size: %d bytes.\n", *target)
	}

	// With Has - the check succeeds.
	if kse, ok := errorx.Has[*aes.KeySizeError](err); ok {
		fmt.Printf("AES keys must be 16, 24, or 32 bytes long, got %d bytes.\n", *kse)
	}
}
Output:

AES keys must be 16, 24, or 32 bytes long, got 31 bytes.

func HasError

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

HasError finds the first error in `err`'s tree that is of type `T`. If a matching error is found, it is returned along with `true`. Otherwise, the zero value for `T` (`nil` in case of a pointer type) and `false` are returned.

This function provides a generic, type-safe alternative to the standard library's `errors.As`.

The error tree is traversed depth-first, starting with `err` itself. The tree is explored by repeatedly calling `Unwrap() error` or `Unwrap() []error`.

An error is considered to be of type `T` if:

  • The error's concrete value is assignable to `T`.
  • The error has a method `As(any) bool`, and calling `As` with a pointer to a value of type `T` returns `true`. In this case, the `As` method is responsible for the result.
Example
package main

import (
	"crypto/aes"
	"fmt"

	"fillmore-labs.com/exp/errors"
)

func main() {
	key := []byte("My kung fu is better than yours")
	_, err := aes.NewCipher(key)

	if kse, ok := errors.Has[aes.KeySizeError](err); ok {
		fmt.Printf("AES keys must be 16, 24, or 32 bytes long, got %d bytes.\n", kse)
	}
}
Output:

AES keys must be 16, 24, or 32 bytes long, got 31 bytes.

Types

This section is empty.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL