An idea to make async Rust more intuitive for beginners

50 points TheVultix

My company has been adopting Rust, making heavy use of async and streams. I keep getting frustration from newer engineers at one particular pattern, that intuitively feels like it should work.

Here's a simple example:

async fn print_async(message: &str) {
    tokio::task::spawn(async {
        println!("{message}");
    })
    .await
    .unwrap();
}

The problem is that tokio's spawn function requires the future to be 'static. This feels frustrating, as it's easy to think "I await right there in the function, so message will live long enough!"

A possible solution?

I'm not even sure if this would be feasible, but could we add a way for the spawn function to associate the lifetime of the input future with the returned JoinHandle?

Instead of this:

pub fn spawn<T>(future: T) -> JoinHandle<T::Output>
where
    T: Future + Send + 'static,
    T::Output: Send + 'static;

The signature might be something like this:

pub fn spawn<T>(future: T) -> JoinHandle<T::Output>
where
    T: Future + Send + 'await,
    T::Output: Send + 'await;

The idea being that a reserved 'await lifetime signals to rust that the future must live long enough for the returned JoinHandle to be awaited. (We'd of course need to handle multiple return types, IntoFuture, and other problems, this syntax is just to convey the concept)

Something like this would be especially useful when working with streams, and would hopefully fill another of those "it feels like this should work" moments.