Async Rust
Course 2 Chapter 5 covered real OS threads. This chapter covers Rust's other concurrency model: async/.await, for cases where spinning up a full OS thread per task is too heavy — many concurrent I/O-bound operations like network requests or file I/O.
async fn and Futures
Calling an async fn does not run its body immediately — it returns a Future, a value representing "this computation, not yet done." A Future does nothing until it's actually driven forward. This is a genuinely different mental model from JavaScript Promises, which start running eagerly the moment they're created — Rust Futures are lazy, a real, easy-to-miss difference for anyone coming from JS's async model.
.await: Driving a Future to Completion
.await can only be used inside an async fn or async block. It suspends the current async function until the awaited Future resolves — without blocking the underlying OS thread; other tasks can run on that same thread while this one is suspended. That's the core efficiency win: many tasks sharing few OS threads.
Rust Has No Built-In Async Runtime
Course 1/2's std::thread::spawn needed no external crate. Async is different: the standard library defines the Future trait and the async/.await syntax, but includes no runtime to actually run futures — an external crate is required. tokio is the dominant choice:
A genuinely important, sometimes-surprising fact: an async fn called with no runtime present, and its Future never awaited, literally does nothing — the future is simply dropped, unpolled.
tokio::spawn: Concurrent Async Tasks
Analogous to Course 2's thread::spawn, but spawns a lightweight async task onto tokio's own scheduler, not a full OS thread — thousands of these can run cheaply, unlike OS threads. .await on the handle echoes Course 2's .join(), now async.
Contrast With Node's Event Loop (js3-3)
Node's event loop is single-threaded, with an implicit runtime always running, built directly into the platform. Rust's async has no implicit runtime at all — one must be explicitly chosen and started (like tokio) — and tokio itself is typically multi-threaded by default, unlike Node's single JS thread. Genuinely different concurrency models sitting underneath a superficially similar async/await syntax both languages happen to use.
Contrast With Go's Goroutines (Course 2, go2-3)
Go's goroutines are the closest existing comparison in this course — also lightweight, also cooperatively scheduled under the hood. But Go's goroutines need no special syntax at the call site at all (go someFunc(), and ordinary-looking blocking code just works via the runtime's scheduler). Rust's async is explicit and colored — an async fn can genuinely only be awaited from another async context, the well-known "function coloring" critique of async/await as a language feature in general, not unique to Rust. Go deliberately avoided this entirely by never requiring a different function signature for concurrent code — a real, fair trade-off worth naming honestly, not a case where either design is simply better.
| Go Goroutines | Rust Async | |
|---|---|---|
| Call-site syntax | go someFunc() — no special function signature | async fn + .await — "colored" functions |
| Runtime | Built directly into the language | None built-in — must choose one (e.g. tokio) |
| Blocking-looking code | Just works via the runtime's scheduler | Must be async-aware throughout the call chain |
async fn
Returns a lazy Future immediately — the body doesn't run until awaited.
Future (Lazy)
A value representing "not yet done" — genuinely different from JS's eager Promises.
.await
Suspends without blocking the OS thread — other tasks run in the meantime.
tokio::spawn
A lightweight async task, not a full OS thread — cheap enough to spawn thousands.
.await compiles fine — it just produces a Future value that's immediately dropped, and the function's body never runs at all. The compiler does emit a warning ("unused implementer of Future that must be used" or similar), but it's easy to miss, and the resulting bug — something that looks like it should have happened, silently didn't — can be genuinely confusing to diagnose. A real, common, async-specific mistake.
Coding Challenges
Write an async function greet(name: &str) -> String returning a greeting, and a #[tokio::main] async fn main() that awaits it and prints the result. Explain what would happen (or rather, not happen) if the .await were removed.
📄 View solutionUsing tokio::spawn, write code that spawns 3 concurrent async tasks (each simulating work with a short tokio::time::sleep), awaits all 3 handles, and prints a message once all have completed.
📄 View solutionExplain the "function coloring" problem in your own words, using async fn as the example — specifically, why an ordinary, non-async function generally can't call .await on a Future the way an async function can.
📄 View solutionChapter 3 Quick Reference
- async fn — returns a lazy Future immediately; the body doesn't run until awaited
- .await — suspends the current async function without blocking the OS thread
- Rust ships no built-in async runtime — an external crate like tokio is required
- tokio::spawn — a lightweight async task, analogous to thread::spawn but far cheaper
- Node's event loop is single-threaded and implicit; tokio is typically multi-threaded and must be explicitly started
- Go's goroutines require no special function signature; Rust's async fn is "colored" and needs an async context throughout
- Async suits many I/O-bound tasks; OS threads suit CPU-bound or small-fixed-worker-count parallel work
- Forgetting .await compiles but silently does nothing — the future is dropped, unpolled
- Next chapter: Macros — declarative macros (macro_rules!) and a brief intro to procedural macros