Overview
5 min read
In TypeScript you reach for async/await and Promise constantly, and the Node.js event loop runs them for you — invisibly, on a single thread. Rust has the same async/await syntax, but the model underneath differs in one decisive way: a Rust Future is lazy. Where a JavaScript Promise starts running the moment you create it, a Rust future does nothing until you .await it (or hand it to a runtime), and Rust ships no built-in runtime — you choose one, almost always Tokio. This section maps every async habit you have — Promise.all, Promise.race, async iterators, shared mutable state — onto its idiomatic, compile-verified Rust counterpart, and is careful to flag exactly where the analogy with JavaScript breaks down.
What You’ll Learn
Section titled “What You’ll Learn”- The single most important shift: a
Promiseis eager (runs on creation) while aFutureis lazy (runs only when awaited/polled), and why that means Rust needs an explicit runtime - How
async/awaitsyntax maps over — anasync fnreturnsimpl Future,.awaitreplacesawait, and?still propagates errors - Why Node’s single-threaded event loop becomes the Tokio runtime, and the difference between the multi-thread and current-thread schedulers
- How to set up a Tokio project (
#[tokio::main],features = ["full"]) — all examples compile-verified against current Tokio - That
async fnin traits is native and stable (since Rust 1.75) — you only need theasync-traitcrate fordyn Trait(dynamic dispatch) - How
AsyncIteratorbecomes theStreamtrait, and how to consume one withStreamExt/while let - How
Promise.race/Promise.allbecometokio::select!/join!/try_join! - The async channel family (
mpsc,oneshot,broadcast,watch) for moving data between tasks - How
tokio::spawnrelates to firing off aPromise, what aJoinHandleis, and when to usespawn_blocking - Concurrency vs parallelism, tasks vs OS threads, and the
Arc<Mutex<T>>pattern for shared mutable state across tasks - When async is the wrong tool — CPU-bound work, the “function coloring” problem, and choosing threads instead
Topics
Section titled “Topics”| Topic | Description |
|---|---|
| Promises vs Futures | The key difference: eager JS Promise vs lazy Rust Future, which does nothing until awaited and needs a runtime. |
| Async/Await | async/await syntax: an async fn returns impl Future, .await, and error handling with ?. |
| Tokio Intro | Node’s event loop → the Tokio runtime; why Rust needs an explicit runtime; multi-thread vs current-thread schedulers. |
| Tokio Setup | Adding Tokio (features = ["full"]), #[tokio::main], and the runtime builder. |
| Async Functions & Blocks | async fn and async blocks, capturing, returning futures, and lifetimes in async code. |
| Async in Traits | Native async fn in traits (stable since 1.75) and when you still need the async-trait crate (for dyn). |
| Streams | AsyncIterator → the Stream trait; consuming with StreamExt / while let. |
| select! and join! | Promise.race/Promise.all → tokio::select! / join! / try_join!. |
| Channels | Async channels: mpsc / oneshot / broadcast / watch, and how they differ from std::sync::mpsc. |
| Spawning Tasks | tokio::spawn, JoinHandle, tasks vs OS threads, and spawn_blocking for blocking work. |
| Concurrency | Concurrency vs parallelism, tasks vs threads, structured patterns, and cancellation. |
| Synchronization Primitives | Tokio Mutex/RwLock/Semaphore vs their std versions, and the danger of holding a lock across .await. |
| The Arc<Mutex<T>> Pattern | Sharing mutable state across tasks with Arc<Mutex<T>> / Arc<RwLock<T>>. |
| Async vs Sync | When to use async vs threads vs blocking: CPU-bound vs IO-bound, and the “function coloring” issue. |
Learning Objectives
Section titled “Learning Objectives”By the end of this section, you will be able to:
- Explain why a Rust
Futuredoes nothing until awaited, and contrast that with an eager JavaScriptPromise - Set up and run a Tokio application, choosing the right scheduler and feature flags
- Write
async fns, await them, propagate errors with?, and put async methods in traits without reaching for a crate unless you needdyn - Run work concurrently with
join!/try_join!, race withselect!, and move data between tasks over the right kind of channel - Spawn tasks, await their
JoinHandles, and offload blocking/CPU-bound work withspawn_blockingor a thread - Share mutable state safely across tasks with
Arc<Mutex<T>>, and avoid holding a lock across an.await - Decide deliberately between async, threads, and plain blocking code for a given workload
Prerequisites
Section titled “Prerequisites”- Section 08: Error Handling — async code is full of
Resultand?; fallible.awaits propagate errors exactly like synchronous ones. - Section 09: Generics & Traits — a
Futureis a trait,async fnreturnsimpl Future, andSend/Syncbounds decide what can cross task boundaries. - Section 10: Smart Pointers — shared async state is built from
Arcplus aMutex/RwLock, so be comfortable with reference counting and interior mutability first.
Estimated Time
Section titled “Estimated Time”- Reading: 6-7 hours
- Hands-on Practice: 5-6 hours
- Exercises: 3 hours
- Total: 14-16 hours
Tip: Read in order. Start with
promises-vs-futuresandasync-awaitto internalize laziness — the one idea that trips up every JavaScript developer — then set up Tokio before moving on to streams,select!/join!, channels, and shared state. Saveasync-vs-syncfor last: once you understand the machinery, you can judge when not to use it.
Next: Section 12: Modules & Packages → — organizing code with modules and managing dependencies with Cargo, the equivalents of ES modules and npm.