Async/Await and Tokio
Async/await: the most familiar Rust concurrency model for TS developers
Section titled “Async/await: the most familiar Rust concurrency model for TS developers”If you have written TypeScript, you already know the mental model behind Rust async/await. Both languages use the same surface syntax (async fn, .await) to write non-blocking code that reads like sequential logic. The key concepts map almost one-to-one:
| JavaScript | Rust |
|---|---|
async function | async fn |
await somePromise | someValue.await |
Promise<T> | impl Future<Output = T> |
Promise.all(...) | tokio::join!(...) or futures::join_all(...) |
| Node.js event loop (built-in) | Tokio runtime (explicit dependency) |
Promise.allSettled | tokio::join! (all always complete) |
The most important difference: JavaScript’s runtime (V8, Node.js) is always running. In Rust, there is no built-in async runtime — you must bring your own. Tokio is the de facto standard for server-side async Rust (web servers, databases, network services).
async fn and .await
Section titled “async fn and .await”An async fn returns a Future — a lazy computation that does nothing until it is polled by a runtime. You .await a Future to run it and get its result. This is syntactically identical to JavaScript’s await.
// TypeScript async/awaitasync function fetchUser(id: number): Promise<string> { const resp = await fetch(`/users/${id}`); const data = await resp.json(); return data.name;}
// Run concurrently with Promise.allasync function main() { const [user1, user2] = await Promise.all([ fetchUser(1), fetchUser(2), ]); console.log(user1, user2);}use tokio::time::{sleep, Duration};
// async fn returns a Future<Output = String>async fn fetch_user(id: u32) -> String { // .await yields control to the runtime while "waiting" sleep(Duration::from_millis(10)).await; format!("user_{id}")}
#[tokio::main] // sets up the Tokio runtime for main()async fn main() { // tokio::join! runs both futures concurrently on the same thread pool // — equivalent to Promise.all let (user1, user2) = tokio::join!( fetch_user(1), fetch_user(2), ); println!("{user1} {user2}"); // user_1 user_2}Spawning async tasks (tokio::spawn)
Section titled “Spawning async tasks (tokio::spawn)”tokio::spawn is the async equivalent of thread::spawn. It schedules a future on the Tokio thread pool and returns a JoinHandle<T>. Unlike thread::spawn, the task runs on a thread pool managed by Tokio — typically one thread per CPU core.
// TypeScript: fire-and-forget with Promise (or await to collect result)async function work(id: number): Promise<number> { await new Promise(resolve => setTimeout(resolve, 10)); return id * 2;}
async function main() { // Spawn all, then await all — equivalent to tokio::spawn + join const handles = [1, 2, 3].map(id => work(id)); const results = await Promise.all(handles); const total = results.reduce((a, b) => a + b, 0); console.log('total:', total); // total: 12}use tokio::time::{sleep, Duration};
async fn work(id: u32) -> u32 { sleep(Duration::from_millis(10)).await; id * 2}
#[tokio::main]async fn main() { let handles: Vec<_> = (1u32..=3) .map(|id| tokio::spawn(work(id))) .collect();
let mut total = 0u32; for h in handles { total += h.await.unwrap(); } println!("total: {total}"); // total: 12}Try it
Section titled “Try it”use tokio::time::{sleep, Duration};
async fn fetch_user(id: u32) -> String { sleep(Duration::from_millis(10)).await; format!("user_{id}")}
#[tokio::main]async fn main() { let (user1, user2) = tokio::join!( fetch_user(1), fetch_user(2), ); println!("{user1} {user2}");}Compiling…