Clippy suggests broken code (async fn)

Hi everyone!

This is my first post here. Not sure if this is the right place to post this, so if it's not, please let me know!

I encountered compiler errors about a missing Send trait of the future produced by an async fn. To my surprise, I could fix that by turning the async fn into an fn with an async block. I could reproduce my problem with some minimal code.

Let's start with code that compiles just fine:

type R = Result<(), tokio::task::JoinError>;

#[tokio::main(flavor = "current_thread")]
async fn main() -> R {
    println!("Hello, world!");
    outer(true).await?;

    Ok(())
}

fn outer(recurse: bool) -> impl std::future::Future<Output = R> + Send {
    async move {
        if recurse {
            tokio::spawn(inner()).await??;
        }

        Ok(())
    }
}

async fn inner() -> R {
    outer(false).await
}

However, if I run cargo clippy, it suggest to me that I really should use an async fn for outer:

warning: this function can be simplified using the `async fn` syntax
  --> src/main.rs:11:1
   |
11 | fn outer(recurse: bool) -> impl std::future::Future<Output = R> + Send {
   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |
   = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#manual_async_fn
   = note: `#[warn(clippy::manual_async_fn)]` on by default
help: make the function `async` and return the output of the future directly
   |
11 | async fn outer(recurse: bool) -> R {
   | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
help: move the body of the async block to the enclosing function
   |
11 ~ fn outer(recurse: bool) -> impl std::future::Future<Output = R> + Send {
12 +     if recurse {
13 +         tokio::spawn(inner()).await??;
14 +     }
15 +
16 +     Ok(())
17 + }
   |

I would love to do that, it looks much cleaner. However, if I apply those changes, my code looks like so:

type R = Result<(), tokio::task::JoinError>;

#[tokio::main(flavor = "current_thread")]
async fn main() -> R {
    println!("Hello, world!");
    outer(true).await?;

    Ok(())
}

async fn outer(recurse: bool) -> R {
    if recurse {
        tokio::spawn(inner()).await??;
    }

    Ok(())
}

async fn inner() -> R {
    outer(false).await
}

...and this doesn't compile. This is what cargo check has to say:

error: future cannot be sent between threads safely
   --> src/main.rs:13:22
    |
13  |         tokio::spawn(inner()).await??;
    |                      ^^^^^^^ future returned by `inner` is not `Send`
    |
note: opaque type is declared here
   --> src/main.rs:11:1
    |
11  | async fn outer(recurse: bool) -> R {
    | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
note: this item depends on auto traits of the hidden type, but may also be registering the hidden type. This is not supported right now. You can try moving the opaque type and the item that actually registers a hidden type into a new submodule
   --> src/main.rs:11:10
    |
11  | async fn outer(recurse: bool) -> R {
    |          ^^^^^
note: future is not `Send` as it awaits another future which is not `Send`
   --> src/main.rs:20:5
    |
20  |     outer(false).await
    |     ^^^^^^^^^^^^ await occurs here on type `impl Future<Output = Result<(), JoinError>>`, which is not `Send`
note: required by a bound in `tokio::spawn`
   --> /home/sven/.cargo/registry/src/index.crates.io-6f17d22bba15001f/tokio-1.36.0/src/task/spawn.rs:166:21
    |
164 |     pub fn spawn<F>(future: F) -> JoinHandle<F::Output>
    |            ----- required by a bound in this function
165 |     where
166 |         F: Future + Send + 'static,
    |                     ^^^^ required by this bound in `spawn`

As a side note: when I run cargo clippy --fix on the original code, clippy notices there is a problem with the fix it suggests and doesn't save it to the file. I ran cargo clippy --fix --broken-code to obtain the "fixed" version.

I don't think this is a Clippy bug, though. The suggest fix should work, shouldn't it? Isn't this a compiler problem?

Thanks everyone for any help. Much appreciated!

Correctly reasoning about mutual recursion is hard, and the compiler doesn't always get it right. This issue sounds like it might be the same case:

For now, you should silence clippy.

#[allow(clippy::manual_async_fn)]
fn outer(recurse: bool) -> impl std::future::Future<Output = R> + Send {
    ...
1 Like

That case doesn't seem to be about clippy but why the code doesn't compile.

There should probably be a bug about clippy suggesting a change that doesn't compile.

Clippy can't know that this case won't compile without essentially re-running the compiler on the modified code. That would be impractically slow.