While I agree with your approach on a philosophical level, it goes against the established practices of the Go standard library (and community libraries in the style of the Go standard library). It turns out that the opposite approach — “don't repeat information that was supplied to the callee that produced the error” — seems to be equally coherent if applied consistently.
(In my experience the idiomatic Go approach does require more unwrapping and repacking of errors to avoid redundancy, but it at least avoids an impedance mismatch between user APIs and the standard library.)
I think this relies on non-locality in a way that makes it less self-contained/harder to contemplate. If you are the author of a function you know that you have provided sufficient tracing information to figure out where within your function things failed using my method. Whereas if you rely on functions annotating their own function names, then you cannot tell with any certainty from reading the code of a function that this is true. Instead you have to inspect each of the functions you called. In other words, while your approach does lead to sane error messages, I think it’s less readable.
Oh, I definitely agree that it's harder to work with! But it's even harder to work with a package ecosystem in which some packages return errors in one style and others use the opposite style. And as much as you (and I!) may prefer the other approach, the established style for Go is “callee adds information”.
fs.PathError is one of the big ones: the caller pretty much always knows which function or method they called, and for functions like os.Open or os.Create they have the path too. But those functions nonetheless return a PathError that includes both the passed-in path and the name of the operation that failed (which is often redundant with the name of the function itself).
What about the case when func you are calling is hidden behind an interface?
Example:
```
func read(r io.Reader) error {
_, _ := r.Read(x) // here we dont realy know what function we are calling
}
read()
```
I think the opposite approach is more suitable: function should always put information about himself into the error message.
Like this: fmt.Errorf("pkg.fn: %w", err)
Also it's how standart library doing it.
While I agree with your approach on a philosophical level, it goes against the established practices of the Go standard library (and community libraries in the style of the Go standard library). It turns out that the opposite approach — “don't repeat information that was supplied to the callee that produced the error” — seems to be equally coherent if applied consistently.
(In my experience the idiomatic Go approach does require more unwrapping and repacking of errors to avoid redundancy, but it at least avoids an impedance mismatch between user APIs and the standard library.)
I think this relies on non-locality in a way that makes it less self-contained/harder to contemplate. If you are the author of a function you know that you have provided sufficient tracing information to figure out where within your function things failed using my method. Whereas if you rely on functions annotating their own function names, then you cannot tell with any certainty from reading the code of a function that this is true. Instead you have to inspect each of the functions you called. In other words, while your approach does lead to sane error messages, I think it’s less readable.
Oh, I definitely agree that it's harder to work with! But it's even harder to work with a package ecosystem in which some packages return errors in one style and others use the opposite style. And as much as you (and I!) may prefer the other approach, the established style for Go is “callee adds information”.
See https://github.com/bcmills/go-experience-reports/blob/master/errors.md for more detail.
fs.PathError is one of the big ones: the caller pretty much always knows which function or method they called, and for functions like os.Open or os.Create they have the path too. But those functions nonetheless return a PathError that includes both the passed-in path and the name of the operation that failed (which is often redundant with the name of the function itself).