Course contentsShow
← Rust
Lesson 274 of 1,9103. Borrowing and ReferencesFree lesson

Borrowing within Async Contexts

Navigate the challenges of holding references across await points.

In this lesson, you will learn why holding references across an await point creates unique challenges for the Rust compiler and how it impacts your async code.

The Async Suspension Trap

In synchronous code, a local variable stays in its scope until you exit the function. However, an async function is transformed by the compiler into a State Machine. When you await an asynchronous operation, your function pauses, saves its current state (the variables currently in scope), and yields control back to the executor.

If you have a reference to a local variable and try to keep it across an await point, you might run into trouble.

The Lifetime Problem

Consider this scenario:

async fn process_data(data: &String) {
    let message = format!("Processing: {}", data);
    
    // An await point occurs here
    do_something_async().await; 
    
    // We are now using 'message' after the await
    println!("{}", message);
}

When do_something_async().await is called, the compiler must store the state of the function. If message (or a reference to it) needs to persist across that gap, it becomes part of the Future object's internal structure.

The borrow checker becomes very strict here because the Future might be moved between different threads or held for an unpredictable amount of time while the async operation completes. If the data you borrowed is dropped or modified while the Future is "paused" in the background, you would end up with a dangling pointer or a data race.

Best Practices

To navigate these challenges:

  1. Keep it brief: Try to scope your borrows tightly. If you only need a reference for a short calculation, ensure that reference goes out of scope before you reach an await point.
  2. Use Owned Data: If you find yourself fighting the compiler to keep a reference alive across an await, it is often a sign that you should pass an owned type (like String instead of &str or Arc<T> instead of &T) into the async block. This ensures the data stays alive as long as the Future exists.
  3. Check Send requirements: Remember that if your Future needs to be Send (to run on multi-threaded executors), the types stored across the await must also be Send.

Key Takeaway: Because async functions can pause and resume, any reference held across an await point must be guaranteed to live for the entire duration of the suspended state; when in doubt, prefer cloning or using shared ownership (Arc) to safely move data into the future.