Cryptic async closure error

I cannot decode the cryptic error message from the compiler. I can deduce it is related to the &DbConn. If I use DbConn instead of &DbConn then it works.

#![allow(unused_variables, dead_code)]
#![deny(elided_lifetimes_in_paths)]

struct DbConn;

// Generic fetcher for different types
async fn fetch_connection<F, Fut>(db: &DbConn, fetcher: F)
where
    F: FnOnce(&DbConn, u64, u64) -> Fut,
    Fut: Future<Output = anyhow::Result<Vec<Dummy>>>,
{
}

struct Dummy;
// Fetch all dummies
impl Dummy {
    async fn fetch_all(db: &DbConn, limit: u64, offset: u64) -> anyhow::Result<Vec<Dummy>> {
        Ok(Vec::new())
    }
}

// Actual work
async fn work() {
    let db = DbConn;

    fetch_connection(&db, async |db, limit, offset| {
        Dummy::fetch_all(db, limit, offset).await
    })
    .await;
}

error[E0308]: mismatched types
  --> src/lib.rs:26:5
   |
26 | /     fetch_connection(&db, async |db, limit, offset| {
27 | |         Dummy::fetch_all(db, limit, offset).await
28 | |     })
   | |______^ one type is more general than the other
   |
   = note: expected `async` closure body `{async closure body@src/lib.rs:26:53: 28:6}`
              found `async` closure body `{async closure body@src/lib.rs:26:53: 28:6}`
   = note: no two async blocks, even if identical, have the same type
   = help: consider pinning your async block and casting it to a trait object
note: the lifetime requirement is introduced here
  --> src/lib.rs:9:37
   |
9  |     F: FnOnce(&DbConn, u64, u64) -> Fut,
   |                                     ^^^

error[E0308]: mismatched types
  --> src/lib.rs:26:5
   |
26 | /     fetch_connection(&db, async |db, limit, offset| {
27 | |         Dummy::fetch_all(db, limit, offset).await
28 | |     })
29 | |     .await;
   | |__________^ one type is more general than the other
   |
   = note: expected `async` closure body `{async closure body@src/lib.rs:26:53: 28:6}`
              found `async` closure body `{async closure body@src/lib.rs:26:53: 28:6}`
   = note: no two async blocks, even if identical, have the same type
   = help: consider pinning your async block and casting it to a trait object
note: the lifetime requirement is introduced here
  --> src/lib.rs:9:37
   |
9  |     F: FnOnce(&DbConn, u64, u64) -> Fut,
   |                                     ^^^

error[E0308]: mismatched types
  --> src/lib.rs:29:6
   |
26 |       fetch_connection(&db, async |db, limit, offset| {
   |  _____________________________________________________-
27 | |         Dummy::fetch_all(db, limit, offset).await
28 | |     })
   | |     -
   | |     |
   | |_____the expected `async` closure body
   |       the found `async` closure body
29 |       .await;
   |        ^^^^^ one type is more general than the other
   |
   = note: expected `async` closure body `{async closure body@src/lib.rs:26:53: 28:6}`
              found `async` closure body `{async closure body@src/lib.rs:26:53: 28:6}`
   = note: no two async blocks, even if identical, have the same type
   = help: consider pinning your async block and casting it to a trait object
note: the lifetime requirement is introduced here
  --> src/lib.rs:9:37
   |
9  |     F: FnOnce(&DbConn, u64, u64) -> Fut,
   |                                     ^^^

1 Like

You cannot use the <F, Fut> approach to async functions when the function accepts a general borrowed parameter like this. This is because the &DbConn reference it takes has a lifetime which is allowed to be arbitrarily short, but Fut (which borrows whatever the function was given, for async closures) is constrained to be a type that is the same for all of the call to fetch_connection, and thus not borrow anything that doesn't outlive the particualr call to fetch_connection().

Technique 1: In your example code as written, you can link the lifetime of fetch_connection's &DbConn and fetcher's:

async fn fetch_connection<'a, F, Fut>(db: &'a DbConn, fetcher: F)
where
    F: FnOnce(&'a DbConn, u64, u64) -> Fut,
    Fut: Future<Output = anyhow::Result<Vec<Dummy>>>,
{

In a situation where that doesn’t apply (because the borrowed value was created or mutated by fetch_connection itself, rather than simply passing an existing reference down), you have to stick to approaches that don't involve introducing a Fut type variable. Technique 2: As of Rust 1.85, you can use the new AsyncFnOnce trait in this case:

async fn fetch_connection<F>(db: &DbConn, fetcher: F)
where
    F: AsyncFnOnce(&DbConn, u64, u64) -> anyhow::Result<Vec<Dummy>>,
{

However, if you need a Fut: Send bound, it is currently impossible to express that with AsyncFnOnce. Technique 3: In that case, you need an older trick, a helper trait alias, such as the ones provided by the async_fn_traits library:

async fn fetch_connection<F>(db: &DbConn, fetcher: F)
where
    F: for<'a> async_fn_traits::AsyncFnOnce3<
            &'a DbConn, u64, u64,
            Output = anyhow::Result<Vec<Dummy>>,
            OutputFuture: Send,
        >,
{

But this often can't be actually used because it runs into the limitations of the combination of FnOnce + Future bounds and gives you similar errors than you were actually dealing with.

Technique 4: You can change your async function to not take references. When a value needs to be shared, use Arc<T> instead of &T.

5 Likes

Can you elaborate a bit about the problems that you would run into? I've hit similar limitations in my async code, particularly writing switchyard and resigned myself to copying the various arguments. But if there's a way to make it work, that would be great.

There are two cases:

  • Closures that cannot work because the composition of Fn[Mut] and Future doesn’t express their borrowing pattern; this is the case discussed in RFC 3668 section "Closures cannot return futures that borrow from their captures" where one must use the new AsyncFn[Mut] traits. This doesn't apply to FnOnce since FnOnce always consumes the closure and therefore the future can always capture anything the closure does.

  • Closures where the composition of Fn* and Future is sufficient but the compiler can’t be persuaded that that’s true. I believe OP’s case falls into this category (if you try my “technique 3” code sample, it will fail to compile at the call site). I do not understand the boundaries of this category.

Nice, thanks!