Asynchronous Rust

async Functions

An async function (async fn) is syntactic sugar for a function that returns an anonymous, compiler-generated state machine implementing std::future::Future<Output = Ret>. Calling an async fn does not execute the body of the function immediately—it packages the arguments into an unstarted Future value that only begins executing when .awaited or polled.

How rustc Desugars async fn

Written in Source Code:
  async fn fetch_user_score(user_id: u64) -> u32 {
      println!("fetching user {user_id}");
      100
  }

Compiler Desugaring (Conceptual Equivalent):
  fn fetch_user_score(user_id: u64) -> impl std::future::Future<Output = u32> {
      async move {
          println!("fetching user {user_id}");
          100
      }
  }

Lazy Execution Model of async fn

In languages like JavaScript (async function) or Go (go func()), calling an async function immediately starts executing its body up to the first await point (or schedules it on a global background runtime).

In Rust, async functions are 100% lazy (cold):

  1. Calling let fut = fetch_user_score(42); constructs the state-machine struct on the stack with 0 heap allocations and executes zero lines of fetch_user_score's body!
  2. Because trait Future is marked #[must_use = "futures do nothing unless you .await or poll them"], calling fetch_user_score(42); without .awaiting or binding the returned future emits an immediate compiler warning (unused_must_use).
use std::future::Future;
use std::pin::pin;
use std::task::{Context, Poll, Waker};

async fn compute_report(id: u32) -> String {
    println!("  [inside compute_report({id})] body is now executing!");
    format!("Report #{id}: OK")
}

fn block_on_ready<F: Future>(fut: F) -> F::Output {
    let mut pinned = pin!(fut);
    let mut cx = Context::from_waker(Waker::noop());
    match pinned.as_mut().poll(&mut cx) {
        Poll::Ready(val) => val,
        Poll::Pending => panic!("future was not immediately ready"),
    }
}

fn main() {
    println!("1. Calling `compute_report(7)` (creates Future, runs 0 lines of body):");
    let report_future = compute_report(7);

    println!("2. `report_future` created (size = {} bytes); now polling:", size_of_val(&report_future));
    let output = block_on_ready(report_future);

    println!("3. Output: {:?}", output);
}
1. Calling `compute_report(7)` (creates Future, runs 0 lines of body):
2. `report_future` created (size = 8 bytes); now polling:
  [inside compute_report(7)] body is now executing!
3. Output: "Report #7: OK"

async Blocks (async { ... } and async move { ... })

In addition to async fn, you can write inline async { ... } and async move { ... } expressions anywhere an expression is valid:

Construct Capture Behavior Return Type
async fn name(args) -> T { ... } Moves arguments into the returned future impl Future<Output = T>
async { ... } Captures variables from enclosing scope by minimum needed borrow (&T / &mut T) impl Future<Output = T>
async move { ... } Moves (takes ownership of) captured variables into the future state machine impl Future<Output = T>