Async Functions and Lifetimes
Unlike synchronous functions—which finish executing and release all borrowed reference arguments before returning—an async fn returns an unexecuted Future immediately, and that Future must store all reference arguments inside its state machine until the future is .awaited to completion!
How async fn Captures Input Lifetimes
When you write an async fn that accepts borrowed references (&'a str, &'b [u8]), the returned impl Future captures every input reference lifetime:
Written Signature:
async fn check_credentials(user: &str, token: &str) -> bool
Desugared Signature (Rust 2024 Edition):
fn check_credentials<'a, 'b>(
user: &'a str,
token: &'b str,
) -> impl Future<Output = bool> + use<'a, 'b>
Because user and token are not read until the returned Future is polled, both user and token must remain valid for the entire lifetime of the returned Future—even if you haven't .awaited it yet:
// ❌ does not compile
async fn greet(name: &str) -> String {
format!("Hello, {name}!")
}
async fn broken_lifetime_demo() -> String {
let fut = {
let local_name = String::from("Alice");
// `greet(&local_name)` does NOT read `local_name` yet!
// It returns a Future borrowing `&local_name`:
greet(&local_name)
}; // Error E0597: `local_name` is dropped here while `fut` still borrows it!
fut.await
}
fn main() {
let _ = broken_lifetime_demo();
}
error[E0597]: `local_name` does not live long enough
--> src/main.rs:11:15
|
8 | let local_name = String::from("Alice");
| ---------- binding `local_name` declared here
...
11 | greet(&local_name)
| ^^^^^^^^^^^ borrowed value does not live long enough
12 | }; // Error E0597: `local_name` is dropped here while `fut` still borrows it!
| - `local_name` dropped here while still borrowed
13 |
14 | fut.await
| --- borrow later used here
Detaching a Future from Short-Lived Input Lifetimes (async move Wrapper)
When a caller wants to pass a temporary &str into a function and obtain a 'static future (for example, to pass into tokio::spawn or return from a scope where the &str drops), perform the synchronous copying/preprocessing outside an async move { ... } block and return impl Future<Output = T> + 'static (or + use<>):
use std::future::Future;
use std::pin::pin;
use std::task::{Context, Poll, Waker};
// Performs synchronous `.to_uppercase()` BEFORE returning the `async move` future,
// so the returned `Future` does NOT capture the lifetime of `name: &str`!
fn greet_detached(name: &str) -> impl Future<Output = String> + 'static {
let owned_upper = name.to_uppercase();
async move { format!("Hello, {owned_upper}!") }
}
fn main() {
// `temp_name` is dropped at the end of the inner block, yet `fut` remains valid (`'static`)!
let fut = {
let temp_name = String::from("Alice");
greet_detached(&temp_name)
};
let mut pinned = pin!(fut);
let mut cx = Context::from_waker(Waker::noop());
println!("resolved detached future: {:?}", pinned.as_mut().poll(&mut cx));
}
resolved detached future: Ready("Hello, ALICE!")