Futures and the Async Syntax

Rust’s async programming centers on futures - values that represent work that may complete later. The Future trait provides a common interface for async operations, similar to Promises in JavaScript.

Key concepts:

  • async functions/blocks return futures
  • await polls futures to completion
  • Futures are lazy - they do nothing until awaited
  • Each await point is where execution can pause/resume
async fn fetch_title(url: &str) -> Option<String> {
    let response = http_get(url).await;  // Yield control here
    let html = response.text().await;    // And here
    parse_title(&html)
}

Setup

Create a new project and add the trpl crate, which provides runtime and utility functions:

$ cargo new hello-async && cd hello-async
$ cargo add trpl

First Async Program: Web Scraper

extern crate trpl; // required for mdbook test

fn main() {
    // TODO: we'll add this next!
}

use trpl::Html;

async fn page_title(url: &str) -> Option<String> {
    let response = trpl::get(url).await;
    let response_text = response.text().await;
    Html::parse(&response_text)
        .select_first("title")
        .map(|title_element| title_element.inner_html())
}

Key points:

  • async fn returns impl Future<Output = ReturnType>
  • await is postfix: future.await not await future
  • Network and parsing operations yield control at await points
  • Chaining with await creates readable async code:
extern crate trpl; // required for mdbook test

use trpl::Html;

fn main() {
    // TODO: we'll add this next!
}

async fn page_title(url: &str) -> Option<String> {
    let response_text = trpl::get(url).await.text().await;
    Html::parse(&response_text)
        .select_first("title")
        .map(|title_element| title_element.inner_html())
}

Async Function Compilation

The compiler transforms:

async fn page_title(url: &str) -> Option<String> { /* body */ }

Into roughly:

fn page_title(url: &str) -> impl Future<Output = Option<String>> {
    async move { /* body */ }
}

Running Async Code

main cannot be async - you need a runtime to execute futures:

extern crate trpl; // required for mdbook test

use trpl::Html;

async fn main() {
    let args: Vec<String> = std::env::args().collect();
    let url = &args[1];
    match page_title(url).await {
        Some(title) => println!("The title for {url} was {title}"),
        None => println!("{url} had no title"),
    }
}

async fn page_title(url: &str) -> Option<String> {
    let response_text = trpl::get(url).await.text().await;
    Html::parse(&response_text)
        .select_first("title")
        .map(|title_element| title_element.inner_html())
}

Fix with trpl::run:

extern crate trpl; // required for mdbook test

use trpl::Html;

fn main() {
    let args: Vec<String> = std::env::args().collect();

    trpl::run(async {
        let url = &args[1];
        match page_title(url).await {
            Some(title) => println!("The title for {url} was {title}"),
            None => println!("{url} had no title"),
        }
    })
}

async fn page_title(url: &str) -> Option<String> {
    let response_text = trpl::get(url).await.text().await;
    Html::parse(&response_text)
        .select_first("title")
        .map(|title_element| title_element.inner_html())
}

The runtime manages the state machine that Rust creates from async blocks. Each await point represents where the state machine can pause and resume.

Racing Multiple Futures

extern crate trpl; // required for mdbook test

use trpl::{Either, Html};

fn main() {
    let args: Vec<String> = std::env::args().collect();

    trpl::run(async {
        let title_fut_1 = page_title(&args[1]);
        let title_fut_2 = page_title(&args[2]);

        let (url, maybe_title) =
            match trpl::race(title_fut_1, title_fut_2).await {
                Either::Left(left) => left,
                Either::Right(right) => right,
            };

        println!("{url} returned first");
        match maybe_title {
            Some(title) => println!("Its page title is: '{title}'"),
            None => println!("Its title could not be parsed."),
        }
    })
}

async fn page_title(url: &str) -> (&str, Option<String>) {
    let text = trpl::get(url).await.text().await;
    let title = Html::parse(&text)
        .select_first("title")
        .map(|title| title.inner_html());
    (url, title)
}

trpl::race returns Either<Left(A), Right(B)> indicating which future completed first. Unlike threads, the order of argument evaluation affects which future starts first (non-fair scheduling).

Runtime and State Machines

Async blocks compile to state machines. The runtime (executor) manages these machines:

enum PageTitleState {
    Start,
    WaitingForResponse,
    WaitingForText,
    Complete,
}

At each await point, the future yields control back to the runtime. The runtime can then:

  • Schedule other futures
  • Handle IO completion
  • Resume futures when their dependencies are ready

This cooperative multitasking requires explicit yield points (await) - CPU-intensive work without await points will block other futures.