Get Error::source of Sync + Send and make it Sync + Send?

I've come from this post about the 'static bound on Error::source(). I'm more interested in the lack of Sync + Send bounds.

After writing this, I've seen the suggested topic: How to implement `Error` for sources that are `Send + Sync` which has a simple enough example playground. I have attempted casting in my playground but it won't do the cast for me (I suspected just casting to Sync + Send wasn't safe).

Anyway, let's backtrack a bit. What am I doing?:

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:

pub trait ThreadSafeError: Error + Send + Sync + 'static {}

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 likely suspects 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.


That being said, anyhow is an interesting example, since indeed it does showcase something similar to a dyn Error + Send + Sync + 'static wrapper. So you may want to look at the instances where they call .source() themselves: they do that for their Chain type (from the .chain() method that returns an iterator of dyn Errors (≠ their thread-safe Error type). Notice how such iterator, and the errors it yields, are not thread-safe. Indeed, as I mentioned, it can't be made thread-safe.


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 :slightly_smiling_face:

pub
struct MyError /* = */ (
    Arc<dyn Error + Send + Sync + 'static>,
);

impl MyError {
    pub
    fn next (self: &'_ Self)
      -> Option<MyError>
    {
        if self.0.source().is_none() {
            return None;
        }
        return Some(DeferToSourceError(self.0.clone()).into());

        // where
        struct DeferToSourceError /* = */ (
            Arc<dyn Error + Send + Sync + 'static>,
        );
        
        impl DeferToSourceError {
            fn resolve (self: &'_ DeferToSourceError)
              -> &'_ (dyn Error + 'static)
            {
                self.0.source().unwrap_or_else(|| panic!(
                    "Error: \
                        inconsitent `.source()` return from {:?}: \
                            yielded `Some(…)`, then `None`
                    ",
                    self.0,
                ))
                
            }
        }

        impl Error for DeferToSourceError {
            fn source (self: &'_ DeferToSourceError)
              -> Option<&'_ (dyn Error + 'static)>
            {
                self.resolve().source()
            }
        }
        
        impl Debug for DeferToSourceError {
            fn fmt (
                self: &'_ DeferToSourceError,
                f: &'_ mut fmt::Formatter<'_>,
            ) -> fmt::Result
            {
                Debug::fmt(self.resolve(), f)
            }
        }

        impl Display for DeferToSourceError {
            fn fmt (
                self: &'_ DeferToSourceError,
                f: &'_ mut fmt::Formatter<'_>,
            ) -> fmt::Result
            {
                Display::fmt(self.resolve(), f)
            }
        }
    }
}

(I'd still advocate for just giving up on the source()-querying API to yield a thread-safe entity, though).


A third option, but which is not really usable, would be to "forget" about the Error trait, and have your own, like you started writing:

trait ThreadSafeErrorWithThreadSafeSources : Error + Send + Sync + 'static {
    fn thread_safe_source (self: &'_ Self)
      -> &'_ (dyn ThreadSafeErrorWithThreadSafeSources)
    ;
}
impl<E : ThreadSafeErrorWithThreadSafeSources> Error for E {
    fn source (self: &'_ Self)
      -> &'_ (dyn Error + 'static)
    {
        self.thread_safe_source() as _ /* assumign upcasting works */
    }
}
  • (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 :grimacing:

3 Likes

Thank you so much for this thoughtful answer.

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! :smiley:

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!

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.