hyper: `hyper::Error` prints its first cause in its Display impl, causes duplicate printing in many scenarios
Version hyper v0.14.17
Description
hyper::Error
’s Display
impl will print its immediate cause in a "{description}: {cause}"
format. The problem is this screws with anything else that tries to print an error along with its causes, as the immediate cause is also the returned value from .source()
.
For example, if I were to print an error with tracing
’s built-in support, e.g. error!(err as &dyn StdError, "message")
, the resulting log line includes err=error trying to connect: tcp connect error: Connection refused (os error 61) err.sources=[tcp connect error: Connection refused (os error 61), Connection refused (os error 61)]
. Or if I wrap this in an eyre::Report
and print that with {:#}
(which includes the cause chain), I get error trying to connect: tcp connect error: Connection refused (os error 61): tcp connect error: Connection refused (os error 61): Connection refused (os error 61)
which is even more confusing.
This appears to be an intentional decision, especially as it has a .message()
accessor that returns an impl Display
that is just the message. But it’s one that is very frustrating. Ideally hyper::Error
would just not include its cause, relying on the user to opt in to showing causes. Alternatively it could just skip the first cause in the .source()
accessor, though that means users who want to actually inspect that cause (e.g. to downcast it) can’t (though in the case of the quoted error, its cause is not a public type and so there’s nothing I can reasonably do with it besides just printing it).
Changing this behavior will surprise anyone who is currently working around it, though I suspect it will simply produce better behavior for most users who just log the error and move on. My preferred solution of “just don’t print the cause” does make the message()
accessor pointless, though that could simply be deprecated without having to do a breaking change.
If you really don’t want to make this change, then at least please change the message()
accessor to actually be an impl StdError + Send + Sync
such that I can then pass that to whatever I’m using that logs causes. The resulting error from this would of course retain the initial cause as the source()
. Please also do this even if you decide the right solution is to just skip the initial cause in source()
, so I can still get the formatting I want without having to reinvent error chains (especially when dealing with stuff like eyre::Report
’s {:?}
printing).
About this issue
- Original URL
- State: open
- Created 2 years ago
- Reactions: 2
- Comments: 16 (7 by maintainers)
Logging the error often involves crawling the chain.
slog
supports a#err
modifier that crawls the chain automatically and prints a message likedescription: source1: source2: source3
.tracing
crawls the chain automatically for any field of typedyn Error + 'static
that is logged and prints it likename=description, name.sources=[source1, source2, source3]
.anyhow::Error
andeyre::Report
crawl the chain automatically when printed with{:#}
or{:?}
, the former printing likedescription: source1: source2: source3
and the latter printing in a multi-line format.These are just what I know off the top of my head. Printing the whole chain is extremely common when logging errors.
And I don’t see how this is in conflict with what you want. Your rationale only matters for the
Display
impl, it does not justify the current behavior ofsource()
. You want users who just print the error and not the whole chain to see the immediate cause, I get that. And you can still do that, just fixsource()
to skip that immediate cause and instead print the next one (if any). With that changed, everyone who prints just the error will see the same behavior as today, and anyone who prints the chain will stop seeing duplicated causes.Note my last paragraph. The existence of
message()
as it is today requires me to write my ownError
wrapper just to get proper behavior. And it also doesn’t justify the fact that printing the error chain duplicates the source.To summarize the two reasonable options here:
Display
. Leave that up to the user to choose to do.source()
to skip the immediate cause and returncause.source()
instead. Additionally, changemessage()
to return animpl Error + Send + Sync + 'static
instead that retains the current cause as its source. That way I can just pass that to my error reporting infrastructure and I will get proper handling of the error chain.@davidpdrsn I just looked myself as well and
ConnectError
does appear to be the only one, thoughis_connect()
doesn’t guaranteeConnectError
.In any case, your solution will lose the message “tcp connect error”.
Unfortunately I can’t wrap this error in another error type that formats the
ConnectError
but skips its first source, unless I want to wrap thehyper::Error
in something likestruct Wrapper { inner: Arc<hyper::Error>, cause: Option<Wrapper2> }
where the second wrapper indirects into the source (the reason being thatsource()
returns a reference and so I can’t construct the wrapper on the spot, which means I need to pre-construct it with access to the error). This is very awkward. It would be easier ifhyper::Error::message()
didn’t keep a borrow on the error (which it doesn’t even need, it’s actually returning a static string, but the declared types don’t reflect that).Edit: I suppose I overlooked the idea of just pre-formatting the message into a
String
and holding onto that, thus allowing us to wrap thehyper::Error
for theConnectError
workaround up front without usingArc
. Still, this is super awkward.Edit 2: Oh, even better, my wrapper can just contain another wrapper around the error and access the message through that second wrapper, and that second wrapper can then do the
ConnectInfo
workaround. Still, super hacky.Sorry for the confusion, I was in fact advocating for the way hyper currently is. I think the solution is instead that the errors working group and error reporting crates figure out a way to indicate they are going to crawl the source chain and print each out. As I mentioned, I believe it is more common for users to just print the
Display
of the error and be done with it, so I that’s why I propose it should be the default.I explored that some in https://github.com/seanmonstar/errors/issues/1. Re-reading that issue, I see it disagrees slightly with my current opinion on what is default. But the rest is still good.
I provide the rationale in this comment here. The short answer is that I believe the more common case is users that are simply
debug!("{}", error)
printing, and not using one of the fancier error reporting crates. Those users would lose out if the behavior were changed.Please give the benefit of the doubt. We all want what’s best, even when what’s best isn’t crystal clear yet.