-
Notifications
You must be signed in to change notification settings - Fork 697
Add Error.Trace*(), and make Wrap/WithMessage never nest. #144
Comments
“the preferred method is to use errors.Wrap(), not errors.WithMessage().” False. Is it by far the most common? Yes. But it is not the “preferred method”. “Perhaps there could be a more intelligent automatic way to choose between errors.Wrap() or errors.WithMessage(), but what would that look like?” It’s called a programmer, and it is a human being capable of intelligent decision making. “that is, we're supposed to switch on err behavior (or err concrete type, though it's not preferred)“ False, we want to test for implementation of interfaces:
https://dave.cheney.net/2016/04/27/dont-just-check-errors-handle-them-gracefully |
Yes, that's obvious. Here's the point I'm trying to make: Earlier, you wrote in another thread that:
If you're saying that the original cause of the error happened to use But midway in the stack, a caller doesn't know whether the "underlying" cause happened to capture the stack with So if you have a big program with many nested function calls to create a large stack of length N, and they all used
Ok, so say you are given
That's what switching on "behavior" means. (Did you know that you could switch on interfaces?) |
@puellanivis I just found 4 existing issues actually related to the problem that this proposal fixes. |
False. If you are calling a package and you need to know the stacktrace at your point for debugging, you call Wrap, if not, you do not, then you don’t need to use Wrap. What good will it do you to trace an errors.New stack to someone else’s package? Unless you intend to debug someone else’s package.
Check the dependencies of the package you’re calling. If it calls into
No, I patched a subtle shadowing bug in the RPC code at Google, and didn’t learn that I could use an Interface in a type assertion. |
And two of them are closed, and others are unimplementable. |
You've conveniently ignored my point about O(N) stack traces.
If there were traces left in by someone else's package, they were left there because it would actually be useful for debugging. If we followed the tracer convention of calling var err = externalFunction()
switch err.(type) { ... }
return What's the point of allowing externalFunction to capture the stack-trace including the ancestral callers, when the error is going to be handled completely without that info? Replace "someone else's package" with "your own external package". "What good will it do you to trace an errors.New stack to your own external package? Unless you intend to debug your own external package." Well yeah... If you don't want that information, maybe you can nuke it with TraceNuker. Maybe
Can't tell if you're being sarcastic...
Dontcare, and false. See "Partial solution" section. |
Yes, I am being sarcastic, because apparently you think I don’t know that a type switch can use interfaces.
No, that is not false. The two that are open are unimplementable, and I have commented about how they are so. I understand, you’re attached to this idea, but it is not a good proposal. Especially as a universal change to the language itself. I‘ve worked on this issue for over 2 years now, trying to come up with the ideal solution, the solution as currently available is the best available, and changing the language to “fix it” is not always the right solution. |
I showed you a type-switch on an interface, even pointed out that type-switching on the concrete type is possible but not preferred, and you said that I was wrong, and showed a non-type-switch version of the same concept. It's a natural conclusion to draw.
I've shown you the implementable solution, it's under section "Partial solution". You'll have to find a flaw in that solution. |
@jaekwon @puellanivis time for a time out please. Please revisit this discussion in the new year once you’ve calmed down. Thank you |
Thanks for moderating, Dave. Sorry for getting the convo heated, Cassondra. Many thanks to issue #144. I agree that full stacktraces are also nice, in addition to tracing. I've taken all the concepts here and implemented them here: https://github.com/tendermint/tmlibs/blob/develop/common/errors.go For your reference, here's a concrete case of extending tmlibs/common.Error: cosmos/cosmos-sdk#766 Any feedback appreciated! |
I just stepped onto it as well: the "idiomatic" use of this package makes the original stack trace unavailable. The origin of the problem still is easy to guess from the error message (thanks to the wrapping), but it overwriting the stack renders the whole idea of having the stack at all useless. |
How does using this package make the original stack trace unavailable. The goal of this package is to preserve the original error. |
@davecheney if you package main
import (
"github.com/pkg/errors"
)
func foo() error {
return fmt.Errorf("foo")
}
func bar() error {
return errors.Wrap(foo(), "bar")
}
func baz() error {
return errors.Wrap(bar(), "baz")
}
func main() {
err := baz()
// extract the original error stack here (expected to get the deepest stack available)
} If |
Currently github.com/pkg/errors.Wrap/Wrapf functions overwrite already attached stack trace. From a UX/DX point of view one would like to add stack trace to an error if none is attached yet. There are several open issues discussing the problem in the original repo: pkg/errors#75 pkg/errors#144 pkg/errors#158 At the moment there is no solution provided, but it looks like the author is willing to make some changes. Until then this commit adds custom Wrap/Wrapf functions which implement this desired behaviour. Other than that, they behave the same way. There is also a pending PR in the original repo: pkg/errors#122 It worths mentioning that there are alternative solutions, like merging stack traces: srvc/fail#13 After examining the solution it seems that it's probably too complicated and unnecessary for most cases. It's also unlikely that the original errors package will implement this behaviour, so for now we stick to the most expected behaviour.
Even if you generate a stack trace at the source, callers of a function (which returns an error) will in general want to wrap it again to add more context, and the preferred method is to use
errors.Wrap()
, noterrors.WithMessage()
.Perhaps there could be a more intelligent automatic way to choose between
errors.Wrap()
orerrors.WithMessage()
, but what would that look like? The only obvious 100%-correct-general-solution given the state ofgithub.com/pkg/errors
today (without assuming anything about the implementation ofexternalFunction() error
) is to always useerrors.WithMessage()
but include minimal trace information like the filename and line number.So OK, if
github.com/pkg/errors
starts advertisingerrors.WithMessage()
as the primary method of adding contextual information, and its implementation is changed to include a modicum of trace information (filename, lineno), (or iferrors.Wrap()
were changed to include only minimal trace information), then we're 89% of the way there. But there's still a problem.In general, it breaks the intent of Golang to wrap an error with
github.com/pkg/errors.Error
, because it breaks the one way that we're supposed to deal with error control flow... that is, we're supposed to switch onerr
behavior (orerr
concrete type, though it's not preferred).And
github.com/pkg/errors
violates that. There existserrors.Cause()
, but now you have to dig down to the first non-errors.Error
-error before you can type-check like above. And you can't just take the root-most cause of an error unless you preclude other custom error types from having a cause.The solution is to not wrap an error, but to add trace information to the same error object. Ultimately it is
error
itself that needs to be a tracer.Partial solution
If
github.com/pkg/errors
were to always keep a single (non-nested)Error
by returning the samegithub.com/pkg/errors.Error
uponerrors.Wrap()
anderrors.WithMessage()
, then we can switch confidently on behavior:The name for
errors.Unwrap()
is tricky... It can't beerrors.Cause()
because if it were a non-github.com/pkg/errors.Error
causer error, we'd be switching on the wrong error. SoUnwrap
seems like a better name.The problem is that it isn't 100% consistent with
errors.Wrap()
, because you only need to unwrap once what was wrapped a million times. Maybeerrors.Wrap()
should be deprecated, anderrors.TraceWithError(error) error
could ensure that the errorerr
is already aerrors.Error
(in which case it would pass it through after callingerr.Trace()
) or else it would returnerrors.Wrap(err)
. Maybeerrors.Unwrap()
should be callederrors.MaybeUnwrap()
.Original discussion: golang/go#23271 (comment)
The text was updated successfully, but these errors were encountered: