I've created some wrappers around Boxed errors similar to anyhow and want to be able to map an error's source to one of my wrappers. Sounds simple enough:
use std::{error::Error, marker::{Send, Sync}};
struct MyBoxedError(Box<dyn Error + Sync + Send +'static);
pub fn source_my_boxed_error(
e: impl Error + Sync + Send + 'static,
) -> MyBoxedError {
// unwrap: this is not panic-safe!
MyBoxedError(Box::new(e.source().unwrap()))
^^^^^^^^^^
}
but because my boxed errors require Sync + Send, I get compiler errors:
`dyn std::error::Error` cannot be shared between threads safely
the trait `Sync` is not implemented for `dyn std::error::Error`
required because of the requirements on the impl of `Send` for `&dyn std::error::Error`
required for the cast to the object type `dyn std::error::Error + Send + Sync`
I understand why this is the case, not every error should have to implement Send + Sync (PoisonError comes to mind...). But I like (the idea of...?) having Sync + Send errors so I can use them in multithreaded contexts, like a web server. I'd be open to reasons why I don't need those bounds, I sort of followed anyhow's lead there.
What could the workaround be for me here?
I thought about creating a supertrait of error, like ThreadSafeError:
I'm not sure I can or should try to coerce arbitrary errors into this type though!
I don't fully understand how anyhow does it, but I can spot likelysuspects within its source.
So to summarize, I'd like to be able to cast the source of a dyn Error + Sync + Send + 'static to be the same type (Error + Send + Sync + 'static) but rust won't let me. Am I doomed to not be able to do this or is there a clever workaround I haven't thought of? Also, how does anyhow accomplish (things like) this?
So, the threads you linked to have the "opposite" problem: they have a thread-safer error, already dyn-erased as one (dyn Error + Send + Sync), but they want to be compatible with an API of errors dyn-erased as "don't care about thread-safety / may not be thread-safe" types (dyn Error, which could be read as dyn Error + ?Send + ?Sync, which by virtue of allowing originally non-Send or non-Sync inhabitants to be coerced / type-erased to that type, must conservatively be non-Send and non-Sync itself.
So, they wanted to "forget" / "hide" their being thread-safe, in a rather unrecoverable manner, may I add (only way to undo such a thing would be through a successful downcasting to a known thread-safe type).
You, in your case, are trying to achieve the opposite: from a potentially non-thread-safe error (the only one you can get your hands on, since that's all the the stdlib Error trait has to offer), you'd like to have a thread-safe error (the one you are wrapping). This is just not possible generally, since that would be an unsound operation (even if the outermost Error is thread-safe, it may very well have a source() which isn't).
This is, again, an instance of converting from their thread-safe Error type to the stdlib box-dyn-ed errors, which may or may not be thread-safe, so it's an instance of loosening the thread-safety requirements.
A kind of trick you could do, since the outermost error is thread-safe, and since you do have an API to transitively query the sources as many times as you want, would be to create a wrapper Error object / instance, which would hold the original Error object (allowing it to be thread-safe), and yet featuring an Error implementation by delegating to / proxying through its .source(), queried on every function call. It won't be super-performant, but querying a .source()Error's API shouldn't be happening frequently enough for that to matter
(if upcasting doesn't work, then there is a workaround I can detail that would make that work).
The idea would then be that people would have to implement the ThreadSafeErrorWithThreadSafeSources trait (feel free to rename it to smth shorter, ofc._) rather than the Error trait (which would be a corollary / result of / implied by this trait). That is such an ergonomic / API requirement, that despite the beauty / elegance of this approach, I just don't see it panning out in practice
Everything you've said makes sense, and the workarounds are interesting to mull over. It's back to the drawing board for me.
From a little initial tinkering (playground), the second solution seems to work well. I'm not concerned about performance, since as you say, reporting errors shouldn't be the critical path!
I'm also quite interested in the possibility of using anyhow::Context, I'd never considered using custom types to provide "machine readable context". That might be right up my alley in fact!