Rust async
How an async fn becomes a Future
An async fn or async {} block does not run when you call it. The compiler turns the body into a state machine that implements the Future trait, and calling the function only builds one instance of that machine. None of the body has executed yet.
async fn greet() { println!("hi"); }
let fut = greet(); // builds a Future, prints nothing
// hi appears only once fut is driven by .await or an executor
Wrong "Calling an async fn kicks off its work in the background." A future is inert. It makes progress only when something polls it: .await inside another async context, or handing it to an executor such as tokio::spawn. Right constructing a future and running it are two separate steps.
What .await does
.await suspends the current async function until the awaited future is ready and hands the thread back to the executor, so other tasks run in the meantime. It yields cooperatively; it does not park the OS thread the way a synchronous call would.
Awaiting is sequential. Two awaits back to back run one after the other:
let a = fetch("a").await; // completes before b even starts
let b = fetch("b").await;
// concurrent version: drive both at once
let (a, b) = tokio::join!(fetch("a"), fetch("b"));
Between await points the code runs straight through on the executor thread, so those await points are the only places a task can pause and let another in.
You have to bring a runtime
The standard library ships the Future trait, Poll, Context, and Waker, which is the contract, but no executor and no IO event loop. Async code does nothing on its own until you pick a runtime such as Tokio.
A runtime is two halves. The executor owns a queue of ready tasks and calls their poll. The reactor watches the OS for readiness (a socket becoming readable, a timer firing) and wakes the task that was waiting on it, which pushes that task back onto the executor's queue. #[tokio::main] builds a runtime and blocks on your async fn main; tokio::spawn hands a future to the executor as an independent task that runs concurrently with the spawner.
The poll and Waker contract
Future, stabilized in 1.36, has one method: poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output>. It returns Poll::Ready(v) when finished or Poll::Pending when it cannot progress. The rule people miss: on Pending you must register the waker from cx, or the task is never scheduled again and hangs.
// register a waker before returning Pending, so wake() can re-queue this task
let waker = cx.waker().clone();
schedule_wake_when_ready(waker);
Poll::Pending
Wrong "waker.wake() resumes the future." It only pushes the task onto the executor's ready queue, which then calls poll again. Right wake means "poll me again," which is why an idle task costs nothing: the executor sleeps on its queue rather than busy-polling. Re-store cx.waker() each poll, since a task can move between executors with different wakers, and never poll a future after it returned Ready.
Why poll takes Pin
An async block stores the locals live across each await inside its state machine, so a local together with a reference into it becomes self-referential. Moving it after the first poll leaves that internal pointer at the old address, now invalid. Pin<&mut Self> encodes "cannot move once polled." Pin pins the pointee, not the handle, and forbids only moving. Most futures carry no self-reference, so they are Unpin and pinning restricts nothing; the compiler marks only self-referential futures !Unpin. Pin reached stable in 1.33.
Blocking starves the executor
A future's synchronous code runs on the worker thread that polls it. A long CPU loop or a blocking call (std::thread::sleep, sync file or socket IO) between awaits never yields, so every other task on that worker stalls; on a current-thread runtime it freezes the whole program. Offload it with tokio::task::spawn_blocking (a separate pool) or a CPU pool like rayon, and use async APIs like tokio::time::sleep instead. Async pays off for IO-bound, high-concurrency work; CPU-bound work needs real parallelism. A related bound falls out of the same mechanism: a multi-threaded runtime can move a task across worker threads at each await, so tokio::spawn requires the future to be Send, which holds only if every value kept alive across an await is Send.
Cancellation is a dropped future
You cancel a future by dropping it, which stops polling it. A select! drops its losing branches, discarding their in-progress futures; this is why cancel-safety matters, since a dropped read may have already consumed bytes now lost. Drop runs the future's destructors, so cleanup stays synchronous.
Wrong "To stop a tokio::spawned task, drop its JoinHandle." The runtime owns the task, so it keeps running detached; you stop it with handle.abort(). Dropping the handle of a spawned task and dropping a future you await inline land on opposite outcomes.
beta
The interviewer part is in the works.
The diagnostic, personal maps, and AI mock interviews are being finished right now. The notes stay free either way. Leave an email and you'll get the first-cohort invite, plus a month of Pro when it opens.