Build the mental model
Because Lesson 18's `std::thread` costs memory and a context-switch per OS thread, handling thousands of concurrent tasks (like thousands of network requests) that way can get expensive — async/await is the lightweight solution to that scaling problem. Calling an `async fn` doesn't run the code immediately; it returns a `Future` value right away — a Future is lazy, and actual work only starts once something `.await`s it or the runtime polls it, similar to Lesson 16's lazy iterator evaluation. Languages like JavaScript or Go bundle their async runtime (an event loop, a goroutine scheduler) directly into the language itself, but Rust deliberately keeps no runtime in the language core — so that Rust can still be used down to embedded systems, where runtime overhead is unacceptable — meaning async code only actually executes once you depend on an external crate like `tokio` (the most widely used one). `.await` is the point where you wait for a Future to complete without blocking the OS thread — it hands control back to the runtime's scheduler, letting that thread go work on other tasks — so a handful of OS threads can efficiently handle hundreds of I/O-bound tasks (network calls, file reads); CPU-bound work is still better suited to Lesson 18's threads.
Connect it to a real scenario
Previewing Lesson 22's Axum API project, each HTTP handler function must be written as `async fn get_lesson(...)` — handling hundreds of concurrent requests (without blocking an OS thread while waiting on a database query) is exactly what async is for. To read many lesson content files from disk concurrently, build `tokio::fs::read_to_string(path).await` into a `Vec` of futures and await them all together with `futures::future::join_all(...)` — lighter weight than Lesson 18's thread-per-file approach. Placing `#[tokio::main]` on the `main` function auto-sets-up the tokio runtime — without that setup, an `async fn` has no way to actually execute at all.
Try the working example
use tokio::time::{sleep, Duration};
async fn fetch_lesson_stat(id: u32) -> String {
sleep(Duration::from_millis(10)).await;
format!("lesson-{id} ready")
}
#[tokio::main]
async fn main() {
let a = fetch_lesson_stat(1);
let b = fetch_lesson_stat(2);
let (result_a, result_b) = tokio::join!(a, b);
println!("{result_a}");
println!("{result_b}");
}You print "lesson-1 ready" and "lesson-2 ready", completing concurrently in about 10ms instead of 20ms sequentially.5-minute try-it
Write `async fn double(n: u32) -> u32` (with a small sleep inside) — call it concurrently for three input values with `tokio::join!` and print all three results — remember the `#[tokio::main]` setup is required.
One important caution
Trying to run `async fn main()` without `#[tokio::main]` (or another runtime setup) — hitting the "async main function is not supported" compile error, because Rust's language core has no built-in runtime.
Awaiting multiple async tasks one after another sequentially (`a.await; b.await;`) — for actual concurrency, `tokio::join!` or `join_all` is needed; sequential awaiting gives no concurrency at all.