Using errors.As error-prone

285 views
Skip to first unread message

cpu...@gmail.com

unread,
Feb 12, 2025, 3:20:48 AMFeb 12
to golang-nuts
I've had some ongoing confusion about using errors.As (see https://play.golang.com/p/m4_cXCzOViD). The issue I'm having is this part of the contract:

An error matches target if the error's concrete value is assignable to the value pointed to by target

I always expected (yes, I know), that base and pointer receivers are interchangeable. This is not the case (see playground). This seems to make using it error-prone as you'll always need to carefully think about the return type of the function invoked. While the contract is always error, a function may return a base or a pointer error type. This influences how using errors.As must be done.

Are there best practices for:
- return base vs. pointer errors
- crafting  the errors.As target type without inspecting the actual function invoked

...and could it make sense to lessen errors.As to match pointer with non-pointer receivers?

Thanks! 

Axel Wagner

unread,
Feb 12, 2025, 4:06:08 AMFeb 12
to cpu...@gmail.com, golang-nuts
On Wed, 12 Feb 2025 at 09:21, cpu...@gmail.com <cpu...@gmail.com> wrote:
I've had some ongoing confusion about using errors.As (see https://play.golang.com/p/m4_cXCzOViD). The issue I'm having is this part of the contract:

An error matches target if the error's concrete value is assignable to the value pointed to by target

I always expected (yes, I know), that base and pointer receivers are interchangeable. This is not the case (see playground). This seems to make using it error-prone as you'll always need to carefully think about the return type of the function invoked. While the contract is always error, a function may return a base or a pointer error type. This influences how using errors.As must be done.

Are there best practices for:
- return base vs. pointer errors

The best practice is pointer errors. I would have personally preferred to live in a world where value errors are better (in particular, it would be pretty nice to be able to compare full error values using `==` in tests). But the issue you have discovered is pretty much exactly, why pointer errors are better.

If you declare `Error()` on a value receiver, then both `err.(YourErr)` and `err.(*YourErr)` will compile just fine, as value-methods are promoted to the pointer type. But only one of them will succeed. Similarly, both `return YourErr{…}` and `return &YourErr{…}` will compile. So it is somewhat easy to make a mistake that are then hard to debug in practice - and the error paths tend to be badly tested already.

If, on the other hand, you declare it with a pointer receiver, only `err.(*YourErr)` and `return &YourErr{…}` will compile. Meaning the compiler ensures that you write the correct code.

errors.As is ultimately a dynamic extension of this semantic difference, which is the problem you have stumbled on.
 
- crafting  the errors.As target type without inspecting the actual function invoked

...and could it make sense to lessen errors.As to match pointer with non-pointer receivers?

Thanks! 

--
You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.
To view this discussion visit https://groups.google.com/d/msgid/golang-nuts/afe2df8a-9e42-4511-abe6-8e03b906d637n%40googlegroups.com.

Brian Candler

unread,
Feb 12, 2025, 5:07:09 AMFeb 12
to golang-nuts
It's also consistent with the common idiom of errors.New("foo") which returns a pointer (to an unexported type) - although this doesn't appear to be documented, except that "Each call to New returns a distinct error value even if the text is identical"

cpu...@gmail.com

unread,
Feb 14, 2025, 6:11:53 AMFeb 14
to golang-nuts
> The best practice is pointer errors.

If that is an accepted best practice, wouldn't it make sense to have a go vet or linter check that verifies that (at least for public error types)?
It is really not visible from the api and developers will only start thinking about it once they have been bitten.

Imho this needs much better tooling.

Jason E. Aten

unread,
Feb 14, 2025, 8:04:43 AMFeb 14
to golang-nuts
I myself still use the classic string based-errors as 
original designed. These are immutable values that are easy to compare with == 
and search with strings.Contains().

I don't think there is a wide accepted best practice here. There are
libraries like "errors" but to me wrapping errors is gratuitous complexity that
only adds noise.  That's not a general consensus; just one practitioner's opinion.

Axel Wagner

unread,
Feb 14, 2025, 8:25:29 AMFeb 14
to Jason E. Aten, golang-nuts
On Fri, 14 Feb 2025 at 14:05, Jason E. Aten <j.e....@gmail.com> wrote:
I myself still use the classic string based-errors as 
original designed.

I'm not sure what you mean here. From its first commit, the errors package at least returned pointers. That was shortly after the `error` interface was introduced and even before that existed, package like `os` used pointer receivers for their error types.
So, from what I can tell, at least, error types have always been designed using pointer receivers.
 
I don't think there is a wide accepted best practice here.

I strongly disagree. The standard library exclusively returns pointer type (except in `package syscall` I believe). There are good, technical reasons to use pointer receivers. People should use pointer receivers.
 
There are
libraries like "errors" but to me wrapping errors is gratuitous complexity that
only adds noise.  That's not a general consensus; just one practitioner's opinion.

--
You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.

Axel Wagner

unread,
Feb 14, 2025, 8:29:35 AMFeb 14
to golang-nuts
I'll note that `error` is a special case, as it's an interface type that is almost never mentioned via it's implementation, statically. That is, functions never return `*os.PathError` or the like, but they always use `error` as a return type, which is different from `io.Reader`, where e.g. `strings.NewReader` (and similar functions) returns a concrete type.

This makes it the only case where it is kind of important to stay consistent, because you can not get static checking and *have* to rely on type-assertions if you want to figure out whether you got a specific concrete type.

Jason E. Aten

unread,
Feb 14, 2025, 10:18:07 AMFeb 14
to golang-nuts
On Fri, 14 Feb 2025 at 14:24, Axel Wagner wrote:
On Fri, 14 Feb 2025 at 14:05, Jason E. Aten wrote:
I myself still use the classic string based-errors as 
original designed.

I'm not sure what you mean here.

I'm sorry I confused you. I'm not doing anything tricky or sophisticated at all.

I simply meant that I create errors with fmt.Errorf, ala `var ErrCtxRain = fmt.Errorf("context cancelled due to rain")`;
search them with strings.Contains(err.Error(),"i/o timeout")); and compare
them with ==, ala `if err == io.EOF || err == ErrCtxRain { ...`

This is minimum viable error(!) In other words, exactly what the error interface offers, and no more/no less.
Since I integrate multiple 3rd party packages, I've found this to a viable/sane approach to handling
their errors.

Brian Candler

unread,
Feb 14, 2025, 10:21:17 AMFeb 14
to golang-nuts
> That is, functions never return `*os.PathError` or the like

... which of course is a good thing, as nil pointers and nil interfaces are different. This is something that confused me when I first came to Go:


Hence errors are the exception to the general rule of "accept interfaces but return concrete types"

Jason E. Aten

unread,
Feb 14, 2025, 3:20:21 PMFeb 14
to Axel Wagner, golang-nuts

On Fri, 14 Feb 2025 at 14:24, Axel Wagner <axel.wa...@googlemail.com> wrote:
On Fri, 14 Feb 2025 at 14:05, Jason E. Aten <j.e....@gmail.com> wrote:
I myself still use the classic string based-errors as 
original designed.

I'm not sure what you mean here.

I'm sorry to be confusing. I'm not doing anything tricky or sophisticated at all.

Brian Candler

unread,
Feb 14, 2025, 5:28:34 PMFeb 14
to golang-nuts
The original question is whether you should return values or pointers-to-values as errors.

If you're using errors.New() or fmt.Errorf() or similar, then you're actually using a pointer-to-value.

Jason E. Aten

unread,
Feb 14, 2025, 8:47:11 PMFeb 14
to golang-nuts
Ah. Yes, of course. Pointers always.

I was trying to suggest that creating your own error structs 
in the first place opens a needless Pandora's box.
It is just extra trouble that is trivially avoided
if you simply think of errors as strings, and create them only with fmt.Errorf().

Certainly there can be good reasons to do otherwise, but I
feel like beginners would be better off 99% of the time with that guidance.
Reply all
Reply to author
Forward
0 new messages