Asserting async API function is Send with test

Consider a library that uses a lot of async code. The async functions (Futures) must be guaranteed to run in a Send-context. E.g. using tokio::spawn, which requires the Future to be Send.

What kind of test could be written to assert that an async function implements Send?

A simple example can be the following:

pub async fn example() {}
pub async fn example_not_send() {
    // Using Rc (!Send) across an await makes the Future !Send
    let a =  std::rc::Rc::new(5);

mod tests {
    use super::*;
    fn api_is_send() {
        fn is_send<T: Sync>(_t: T) {}

The problem with this test however, is that the test does not compile. The compile error is what we expect, namely that the future is not Send. But, I'm of the opinion that the test should not cause our code to fail to compile. It should only result in a test error when we run the test... So, what I am after is a way to test whether the returned Future implements the Send trait during (test) runtime.

The impls crate goes in the right direction, but we need to know the type beforehand (source):

// Fallback trait for to all types to default to `false`.
trait NotCopy {
    const IS_COPY: bool = false;
impl<T> NotCopy for T {}

// Concrete wrapper type where `IS_COPY` becomes `true` if `T: Copy`.
struct IsCopy<T>(std::marker::PhantomData<T>);

impl<T: Copy> IsCopy<T> {
    // Because this is implemented directly on `IsCopy`, it has priority over
    // the `NotCopy` trait impl.
    // Note: this is a *totally different* associated constant from that in
    // `NotCopy`. This does not specialize the `NotCopy` trait impl on `IsCopy`.
    const IS_COPY: bool = true;


The type of an async fn can not be known, because it is an anonymous type generated by the compiler.

(The reason I am asking for this is that sometimes deep down in the library, something is used that is not Send or a Trait object notation is missing, causing all asyn functions that depend on it to lose their Send trait. This causes the API to fail if used in a multithreading context.)

Tokio has a whole bunch of similar tests which you can find here.


I don't understand why you don't want your tests to pass compilation, but you can use no_run doctests.

Or, if you can use nightly with your tests, you can use specialization (Rust Playground).

1 Like

There isn't much point to check if a Future is not Sync. Making a future that wasn't Sync to begin with unto Sync isn't a breaking change per semver because existing code won't break.

All you need to do is check if futures already Sync stay Sync with your modifications. Your current is_send function works well enough for that.

Once it becomes Sync even by accident making it !Sync back would be a breaking change. With the check you can prevent such unexpected restrictions.

1 Like

Tokio has a change where LocalSet accidentically became Send and Sync, even though this would be unsound. We only barely caught it, and this is what made us introduce the test.

Thanks a lot for everyone chiming in and thinking about this. The tests in Tokio are similar to what I proposed in my OP. The initial solution is good enough:

fn require_fn_to_be_send() {
    fn require_send<T: Send>(_t: T) {}

In short I guess it's about separation of concerns. If a developer happens to make it non-Send, then the test will not compile, instead of a 'nice' failing test. I am now of the opinion that this is a pedantic difference, but a difference nonetheless.

If a developer would want to ignore the test, then it has to be commented out, instead of adding #[ignore] temporarily or permanently if the desired feature is put on a backlog. It can also be that (second or third level) dependencies cause the test to fail to compile, which means an update could result in not compiling the test, catching the developer off-guard.

Regardless, it's perhaps rightly so that what I initially wanted isn't something I actually need. I think the current solution is good enough. Thanks very much for the proposed solution with the specialization feature.

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.