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 futuresawait
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
returnsimpl Future<Output = ReturnType>
await
is postfix:future.await
notawait 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.