Skip to content

Commit e89cedc

Browse files
committed
Release 0.0.3
- add `As` and `AsError` - cache some escaping variables for `As` - documentation - small refactorings Signed-off-by: Oliver Eikemeier <eikemeier@fillmore-labs.com>
1 parent b7efdf7 commit e89cedc

17 files changed

+652
-135
lines changed

README.md

Lines changed: 64 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
[![Go Report Card](https://goreportcard.com/badge/fillmore-labs.com/exp/errors)](https://goreportcard.com/report/fillmore-labs.com/exp/errors)
88
[![License](https://img.shields.io/github/license/fillmore-labs/errors-exp)](https://www.apache.org/licenses/LICENSE-2.0)
99

10-
`fillmore-labs.com/exp/errors` is an experimental Go library that provides two enhanced, generic alternatives to
10+
`fillmore-labs.com/exp/errors` is an experimental Go library that provides four enhanced, generic alternatives to
1111
`errors.As` for inspecting error chains with improved ergonomics and type safety.
1212

1313
## Motivation
@@ -21,17 +21,19 @@ linter.
2121

2222
## Function Overview
2323

24-
This library provides two complementary functions:
24+
This library provides four functions in two complementary pairs, each building on Go's standard `errors.As`:
2525

26-
- **`HasError`** - A drop-in, type-safe replacement for `errors.As` with better ergonomics
27-
- **`Has`** - An enhanced version that also handles pointer-value type mismatches automatically
26+
- **`HasError`** - Ergonomic and type-safe, returns the found error.
27+
- **`Has`** - The `HasError` API plus pointer-value mismatch handling.
2828

29-
| Feature | `errors.As` | `HasError` | `Has` |
30-
| ------------------------------- | ----------- | ---------- | ----- |
31-
| Generic return type ||||
32-
| No target variable needed ||||
33-
| Interface support ||||
34-
| Pointer-value mismatch handling ||||
29+
- **`AsError`** - The classic `errors.As` API with generic type safety.
30+
- **`As`** - The classic `errors.As` API plus pointer-value mismatch handling.
31+
32+
| Feature | `errors.As` | `HasError` | `Has` | `AsError` | `As` |
33+
| ------------------------------- | ----------- | ---------- | ----- | --------- | ---- |
34+
| No target variable needed ||||||
35+
| Pointer-value mismatch handling ||||||
36+
| Type safe generics ||||||
3537

3638
## `HasError` - Enhanced Ergonomics
3739

@@ -128,21 +130,23 @@ func Has[T error](err error) (T, bool)
128130

129131
#### Prevents Common Bugs
130132

131-
This mismatch would silently fail with `errors.As`:
133+
This mismatch would silently fail with `errors.As`. In the example below, `aes.NewCipher` returns an `aes.KeySizeError`
134+
value, but the check incorrectly looks for a pointer (`*aes.KeySizeError`):
132135

133136
```go
134137
key := []byte("My kung fu is better than yours")
135138
_, err := aes.NewCipher(key)
136139

137-
// With errors.As - this check fails silently.
140+
// With errors.As, this check for a pointer type fails because the
141+
// actual error is a value.
138142
var kse *aes.KeySizeError
139143
if errors.As(err, &kse) {
140-
fmt.Printf("Wrong AES key size: %d bytes.\n", *kse)
144+
// This code is never reached.
141145
}
142146

143-
// With Has - the check succeeds.
147+
// With Has, the check succeeds because it handles the pointer-value mismatch.
144148
if kse, ok := Has[*aes.KeySizeError](err); ok {
145-
fmt.Printf("AES keys must be 16, 24, or 32 bytes long, got %d bytes.\n", *kse)
149+
fmt.Printf("Invalid AES key size: %d bytes. Key must be 16, 24, or 32 bytes.\n", *kse)
146150
}
147151
```
148152

@@ -166,6 +170,44 @@ Unlike `errors.As`, interface types provided to `Has` or `HasError` must embed t
166170
if temp, ok := Has[interface { error; Temporary() bool }](err); ok && temp.Temporary() { /* handle temporary error */ }
167171
```
168172

173+
## Classic API with Enhanced Safety
174+
175+
If you prefer the traditional `errors.As` API that uses target variables, this library provides enhanced versions that
176+
add the same benefits:
177+
178+
### `AsError` - Type-Safe `errors.As`
179+
180+
`AsError` provides generic type safety to prevent target variable type mismatches:
181+
182+
```go
183+
func AsError[T error](err error, target *T) bool
184+
185+
// Usage
186+
var myErr *MyError
187+
if AsError(err, &myErr) {
188+
// myErr is guaranteed to be the correct type
189+
// No risk of type assertion bugs
190+
}
191+
```
192+
193+
### `As` - Type-Safe with Flexible Matching
194+
195+
`As` combines type safety with the same pointer-value mismatch handling as `Has`:
196+
197+
```go
198+
func As[T error](err error, target *T) bool
199+
200+
// Usage
201+
var myErr *MyError
202+
if As(err, &myErr) {
203+
// Also handles pointer-value mismatches automatically
204+
// Most robust option with familiar API
205+
}
206+
```
207+
208+
Both functions prevent common type-related bugs while maintaining the familiar `errors.As` API that some developers
209+
prefer.
210+
169211
## Migration Guide
170212

171213
### From `errors.As` to `HasError`
@@ -191,6 +233,13 @@ If you suspect pointer-value mismatches are causing issues, replace `HasError` w
191233
if myErr, ok := Has[MyError](err); ok { /* ... */ }
192234
```
193235

236+
## Further Reading
237+
238+
- Blog: [Understanding Go Error Types: Pointer vs. Value](https://blog.fillmore-labs.com/posts/errors-1/) - Background
239+
on pointer-value type mismatches
240+
- [Go proposal: errors: As with type parameters](http://go.dev/issues/51945) - Community discussion on improving
241+
`errors.As` with generics
242+
194243
## License
195244

196245
This project is licensed under the Apache License 2.0. See the [LICENSE](LICENSE) file for details.

as.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// Copyright 2025 Oliver Eikemeier. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
//
15+
// SPDX-License-Identifier: Apache-2.0
16+
17+
package errors
18+
19+
// As finds the first error in `err`'s tree that has type `T`, and if one is found,
20+
// sets target to that error value and returns true. Otherwise, it returns false.
21+
//
22+
// The tree consists of `err` itself, followed by the errors obtained by repeatedly
23+
// calling its `Unwrap() error` or `Unwrap() []error` method. When `err` wraps multiple
24+
// errors, `As` examines `err` followed by a depth-first traversal of its children.
25+
//
26+
// An error has the type `T` if the error's value is assignable to `T`, including
27+
// cases where there are pointer-value mismatches (e.g., if `T` is `*MyError` but
28+
// the found error is `MyError`, or vice versa), or if the error has a method
29+
// `As(any) bool` that returns `true`. To accommodate pointer-value mismatches
30+
// in `As` implementations, `As` tries different variations of the target type.
31+
// In the latter case, the `As` method is responsible for setting `target`.
32+
//
33+
// An error type might provide an `As` method, so it can be treated as if it were a
34+
// different error type.
35+
//
36+
// As panics if `target` is a nil pointer.
37+
func As[T error](err error, target *T) bool {
38+
if target == nil {
39+
panic("errors: target cannot be nil")
40+
}
41+
42+
var handler altHandler[T]
43+
44+
for err := range DepthFirstErrorTree(err) {
45+
if result, ok := err.(T); ok {
46+
*target = result
47+
48+
return true
49+
}
50+
51+
if handler == nil {
52+
// Lazily initialize the handler only when a direct type assertion fails.
53+
handler = newAltHandler(target)
54+
}
55+
56+
if result, ok := handler.handleAssert(err); ok {
57+
*target = result
58+
59+
return true
60+
}
61+
62+
if x, ok := err.(interface{ As(any) bool }); ok {
63+
// First, try the standard errors.As contract. This works when T matches
64+
// the type expected by the As method.
65+
if x.As(target) {
66+
return true
67+
}
68+
69+
// If the standard call fails, it might be due to a pointer-vs-value mismatch
70+
// between T and the type the As method is designed to handle.
71+
if result, ok := handler.handleAs(x); ok {
72+
*target = result
73+
74+
return true
75+
}
76+
}
77+
}
78+
79+
return false
80+
}

as_errorsas_test.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// Copyright 2025 Oliver Eikemeier. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
//
15+
// SPDX-License-Identifier: Apache-2.0
16+
17+
package errors_test
18+
19+
import (
20+
"testing"
21+
22+
. "fillmore-labs.com/exp/errors"
23+
)
24+
25+
func TestAsAs(t *testing.T) {
26+
t.Parallel()
27+
28+
errValue := MyAsError(8)
29+
errPointer := &errValue
30+
31+
testAsAs(t, "value receiver", errValue)
32+
testAsAs(t, "pointer receiver", errPointer)
33+
}
34+
35+
func testAsAs(t *testing.T, name string, err error) {
36+
t.Helper()
37+
38+
t.Run(name, func(t *testing.T) {
39+
t.Parallel()
40+
41+
t.Run("find MyPointerError", func(t *testing.T) {
42+
t.Parallel()
43+
44+
var e MyPointerError
45+
if !As(err, &e) {
46+
t.Errorf("Expected to find MyPointerError, but didn't.")
47+
} else if e != MyPointerError(8) {
48+
t.Errorf("Expected MyPointerError(8), but got %d", int(e))
49+
}
50+
})
51+
52+
t.Run("find *MyPointerError", func(t *testing.T) {
53+
t.Parallel()
54+
55+
var e *MyPointerError
56+
if !As(err, &e) {
57+
t.Errorf("Expected to find *MyPointerError, but didn't.")
58+
} else if *e != MyPointerError(8) {
59+
t.Errorf("Expected *MyPointerError(8), but got %d", int(*e))
60+
}
61+
})
62+
63+
t.Run("find MyValueError", func(t *testing.T) {
64+
t.Parallel()
65+
66+
var e MyValueError
67+
if !As(err, &e) {
68+
t.Errorf("Expected to find MyValueError, but didn't.")
69+
} else if e != MyValueError(8) {
70+
t.Errorf("Expected MyValueError(8), but got %d", int(e))
71+
}
72+
})
73+
74+
t.Run("find *MyValueError", func(t *testing.T) {
75+
t.Parallel()
76+
77+
var e *MyValueError
78+
if !As(err, &e) {
79+
t.Errorf("Expected to find *MyValueError, but didn't.")
80+
} else if *e != MyValueError(8) {
81+
t.Errorf("Expected *MyValueError(8), but got %d", int(*e))
82+
}
83+
})
84+
})
85+
}

0 commit comments

Comments
 (0)