Pangram verdict · v3.3
We believe that this document is a mix of AI-generated, AI-assisted, and human-written content
AI likelihood · overall
MixedArticle text · 1,891 words · 5 segments analyzed
On AI assistance "Bodh" in "AIBodh" means understanding. Since conversational AI first arrived, the main thing I kept coming back to it for was learning, asking, getting it wrong, asking again, until something finally made sense. That turned into a belief I still hold: used well, AI can genuinely help people learn. So I use it for teaching, but not as a shortcut. Each article still takes me 20 to 25 hours. AI is how I get through the parts of writing that are genuinely hard: finding the example where a concept is the obvious answer, the analogy that clicks, the visual that makes it concrete, the pass that cuts the jargon and the bloat. I verify the code runs, I keep the voice mine, and I cut anything that does not help you understand. The judgment and the corrections stay with me. If anything feels off in any section, let me know on Discord and I'll work on it. Async Rust is usually taught from one of two directions. The async book and the runtime write-ups show you the internals: how Future, poll, and Pin work, and you can even hand-build a tiny executor. Tokio’s guides go the other way, you wire up a real service and it runs. Both are genuinely useful. What is harder to find is the bridge between them, the part that connects understanding how async works to actually shipping with it.
That bridge is what this series is. You build the engine yourself, the future, the waker, the executor, until you understand every piece, and then you drive the real one, Tokio, and recognize those same pieces because you built them.
Couples therapy
This series assumes two things. First, that you’ve written async and await code in JavaScript before. Second, that you’re comfortable with Rust’s basics: structs, enums, associated functions, and closures (all covered in the free chapters of The Impatient Programmer’s Guide to Bevy and Rust).
A quick note on the series: I plan to keep at least 5 to 8 chapters free to read here, with the rest collected in a paid ebook. The free chapters may not follow the numbering in order, so some will land out of sequence.
If you’ve shipped any modern JavaScript, you’ve done this a hundred times:
Pseudocode, don't use.async function getUser() { /* ... */ }const user = await getUser(); // and it just... works
You write async function, add await, and it runs. You never had to think about what runs it, because something always did. The node process ships a built-in event loop: a hidden engine that picks up your Promises and pushes each one forward until it’s done.
In JavaScript you never had to ask who keeps that loop running. But what about Rust? Who actually runs your async code? Or, in Rust’s own terms: who runs your future?
Rust does the opposite, on purpose. It ships no event loop at all. Nothing in the language is sitting around waiting to run your async code. If you want one, you bring it in. Or, the way we’re going to learn it, you build one yourself.
A second job
That sounds like a missing feature. By the end of this series it’ll feel like the opposite. But before we ask who runs it, let’s be precise about what async is.
A normal function is the kind you write every day. You call it, it runs then and there, and by the next line its return value is already sitting in your variable:
fn add(a: i32, b: i32) -> i32 { a + b }let sum = add(2, 3);
An async function doesn’t do that. You call it and it hands you back a placeholder instead of a result. Not the value, but a thing that represents a value that isn’t ready yet, stamped “I’ll be done later.” Every async language has this placeholder; they just use different names for it:
JavaScript calls it a Promise. Rust calls it a Future.
Different words, identical idea: a value that represents work that isn’t finished yet.
Hang on, doesn’t JavaScript have a Future too?
Not as a type. You’ll still hear both words, and they’re not interchangeable: there’s a write side, filled with the value once the work finishes, and a read side, the handle you hold and await. In JavaScript the Promise you await is the read side, which is the one Rust gives its own name: Future.
resolve is the write side; the Promise you await is the read side.const promise = new Promise((resolve) => { setTimeout(() => resolve("the data"), 1000);});const data = await promise;
Rust names the handle you hold a Future, because in Rust you are the one who reads it, by polling, as you’ll see in a moment. So whenever you read Future in this series, picture a Promise that you have to read yourself.
In Node, the instant you call an async function, the placeholder is live. The hidden event loop grabs it and drives it to completion whether you’re watching or not. await is just you asking for the value once it’s ready. The work was always going to happen.
In Rust, calling an async fn does nothing:
async fn get_user() -> User { /* ... */ }let f = get_user();
get_user() handed you a Future and then stopped. The body hasn’t executed. It’s lazy: it just sits there, fast asleep, parked in a variable. It will do absolutely nothing, forever, until something polls it, the technical word for tapping it on the shoulder and asking “can you make any progress?” And since Rust ships no event loop, the thing doing that tapping has to be a runtime like Tokio that you pull in, or, the way we’ll learn it, code you write yourself.
My Future
You are holding this future. What can you actually do with it to get the work done and pull the value out?
Polling A Future gives you exactly one method. You can poll it. Polling is you asking the future a single question, “can you make any progress right now?” The future runs as far as it can, then answers one of two ways: Ready(value) if it finished, or Pending if it had to stop and wait for something.
That phrase, “runs as far as it can,” is the whole idea, so let’s make it concrete with a future that has a real job: checking out a shopping cart. The async fn loads the cart from the database, then calls a payment provider’s API to charge the card, and finally returns a confirmed order. Neither the database nor the payment API answers instantly, so the future has to stop and wait at each of those two calls.
So let’s poll it, and watch how each poll drives the future forward:
Poll #1. The future starts and fires off the database query to load the cart. The database hasn’t replied yet, so it can go no further. It sleeps right there and answers Pending. Poll #2, once the database replies. It resumes from exactly where it paused, takes the cart, and runs on until it calls the payment API. The charge is still in flight, so it sleeps again and answers Pending. Poll #3, once the payment goes through. It resumes one last time, runs clean to the end of the function, builds the confirmed order, and answers Ready(order).
So “runs as far as it can” means this: pick up from wherever you last paused, and go forward until you either finish or hit the next thing you have to wait on. The early polls each end at a wait, so they hand back Pending. The last poll has nothing left to wait for, so it runs off the end of the function and hands back Ready. Most polls hit a wall and say “not yet”. One poll, eventually, runs off the end and says “done”. Async is just that loop: hitting walls, waiting, trying again, until the last poll finally makes it through.
And here is the part that trips people up coming from Node. poll is not just checking on the future, it is what runs it. Calling the async fn ran none of that body. The first poll is what starts it, and each poll after carries it one stretch further. So the rule is blunt: no poll, no progress. A future that is never polled never runs a single line. Nothing is running it in the background. The only thing that moves it forward is you calling poll again.
“Cool, so I just have to call poll. That sounds easy”. Well, let’s have a look at the function signature.
Let’s understand these one by one.
The Future Itself
self: Pin<&mut Self>
Back at the start I asked you to picture a future like a Promise, a representation of work that is not finished yet.
So what is that representation, concretely?
It is a piece of data. When you write an async fn, the compiler turns your code into a value that holds everything it needs to remember to carry on later. The future is that value: your async code, in data form. And as the future runs, poll by poll, that data is what moves forward.
So a future is just a small struct, and every field it holds is its state, the stuff it has to remember between polls.
The diagram below is a way to picture the future. Treat it as a mental model to build your intuition, not the exact, byte-level layout of a real future in memory:
Our checkout future, paused mid-job, is holding things like where it stopped (at “charge card”), the cart it already loaded from the database, and an item it is still pointing at inside that cart. Each poll picks this state up and runs it forward, and since we need to update those fields we ask for writable access through &mut in the first argument.
But there’s a problem. item points back into the future itself, at the cart field sitting right beside it. A value pointing at its own insides. You have probably never written a struct like that, and that is no accident: normal Rust quietly steers you away from self-references. So you have never had to care where a value lives in memory. Move it into a function, push it into a Vec, hand it to a caller, it all just works. But if this future is moved to a new place in memory, cart goes along to the new spot while item keeps pointing at the old one. It is now pointing at empty space. A dangling pointer.
Why would the future move by itself to a new place in memory?
It doesn’t move itself. Your code moves it, the same way any value gets moved in Rust:
Calling the async fn itself. checkout() returns the future to you. That return is already a move. Passing it around. A future is a value, so handing it to any function passes it by value. That is a move. Storing it. Push it into a Vec, or keep it in a struct field. All moves.
I know this is getting tricky. These concepts are genuinely hard to wrap your head around, and they deserve a fuller explanation than I’ll give here.