Why does the closure returning an async block hold a reference passed as a parameter?

Hello,

In the code below, the mutable reference cannot be borrowed because the reference passed as a parameter hasn't been released. Replacing the update_data1 call with update_data2 compiles without any errors. I'm unsure if this is intended.

use std::future::Future;

#[tokio::main]
async fn main() {
    let update_data1 = |c| async move {
        let c: &str = c;
        tokio::fs::write("output.txt", c).await
    };

    fn update_data2(c: &str) -> impl Future<Output = std::io::Result<()>> + '_ {
        async move { tokio::fs::write("output.txt", c).await }
    }

    let mut text = "abcdef".to_owned();

    update_data1(&text).await;

    text.push_str("gh");

    update_data1(&text).await;
}
error[E0502]: cannot borrow `text` as mutable because it is also borrowed as immutable
  --> src/main.rs:18:5
   |
16 |     update_data1(&text).await;
   |                  ----- immutable borrow occurs here
17 |
18 |     text.push_str("gh");
   |     ^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here
19 |
20 |     update_data1(&text).await;
   |     ------------ immutable borrow later used here

Rust Playground

This is an unfortunate limitation of closures: except in certain scenarios, the compiler doesn't recognize that you want to make one generic over lifetimes, i.e., call it multiple times with different lifetimes on the arguments. So it tries to pick a single lifetime that works for all calls to the closure. In this case, the second call tells the compiler that update_data1(&text) takes at least an &'end_of_function str; then, by extension, it requires the first update_data1(&text) to take an &'end_of_function str, which conflicts with the push_str.

That's why replacing it with an async fn(), or a fn() -> impl Future works: functions have no issues being generic over lifetimes. In fact, we can replicate this issue by taking a single instance of the lifetime-generic update_data2 (Rust Playground):

use std::future::Future;

#[tokio::main]
async fn main() {
    fn update_data2<'a: 'a>(c: &'a str) -> impl Future<Output = std::io::Result<()>> + 'a {
        async move { tokio::fs::write("output.txt", c).await }
    }
    let update_data1 = update_data2::<'_>;

    let mut text = "abcdef".to_owned();

    update_data1(&text).await;

    text.push_str("gh");

    update_data1(&text).await;
}
2 Likes

I didn't know the compiler had such fallback behavior. Thank you!

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.