{"slug": "reasoning-about-async-rust-with-state-machines", "title": "Reasoning About Async Rust with State Machines", "summary": "Async Rust converts every async function into a state machine that records where execution stopped and stores needed values, enabling efficient waiting without blocking threads. This internal model helps developers reason about async behavior, debug systematically, and design reliable code.", "body_md": "Chapter 2\n\n# Reasoning About Async Rust with State Machines\n\n## On AI assistance\n\n[Discord](https://discord.com/invite/cD9qEsSjUH)and I'll work on it.\n\nAsync Rust can be frustrating! You write code that looks reasonable, the compiler disagrees, and the explanation does not seem connected to what you were trying to do. You change a few things, create a few new problems, and eventually get it working without really knowing why.\n\nAnd even when it compiles, you may hit a runtime issue where something hangs, never wakes up, or runs in an order you did not expect, and it is not always clear where to start debugging.\n\nThis chapter starts building a model for reasoning about async Rust instead of treating it as a list of rules to memorize. We will develop that model throughout the chapters ahead, but making it instinctive will take time and practice.\n\nHere, the goal is to take the first step: understand how async Rust works internally and begin forming an intuition for why it behaves the way it does. With practice, this understanding will help you design reliable async code, debug it systematically, and reason about performance.\n\nBefore we continue, let’s recap takeaways from [Chapter 1](/posts/async-rust-chapter-1-hands-on-intro-to-async-rust/):\n\n- Async work is represented by a future, and it does not run by itself.\n- Each poll moves the future forward until it finishes or has to wait.\n- After waiting, the future must continue from where it stopped.\n\n## Problem with Waiting\n\nPrograms spend a surprising amount of time waiting: for a database query, a network response, a timer, or a file operation. If a thread remains occupied during that wait, it cannot do anything else. Most of the time, the CPU is not doing useful work. The thread is simply waiting for an answer from somewhere else. Async code can give the thread back while it waits. The runtime can then use that same thread to move other work forward.\n\nImagine a chat server, game server, or API may hold a network connection for every connected client, but most clients are not sending data at the same moment. Their connections spend most of their time waiting for the next message. Async lets a small number of threads work on whichever connections have data ready, instead of keeping one blocked thread waiting for every connected client.\n\nThis does not make CPU-heavy work faster. A large calculation still needs CPU time. Async is most useful when work repeatedly alternates between doing a little computation and waiting on something outside the CPU.\n\nBut giving the thread back creates a new problem: the waiting work must remember enough to continue later. Where did it stop? Which values does it still need? What should run when the answer arrives?\n\nRust solves this by recording where the work stopped and storing the values it will need when it continues. When the work is ready again, Rust uses that saved information to resume from the right place instead of starting over.\n\nTo do that, Rust turns every `async fn`\n\ninto a **state machine**. Each state represents a place where the work can stop and stores what it needs to continue from there.\n\n## State Machine\n\nA state machine describes work as a set of possible states and the events that move it between them. At any moment, the work is in one state. When something happens, it either stays there or moves to another.\n\nThink about an ATM. It might move through these states:\n\nInserting a card moves the ATM from `Idle`\n\nto `Card Inserted`\n\n. Entering the correct PIN moves it to `Authenticated`\n\n. Choosing a withdrawal and an amount moves it forward again. The same input can mean different things in different states: pressing a number may enter a PIN in one state and choose an amount in another.\n\nEach state also stores what the next step needs. `Card Inserted`\n\nkeeps the card details. `Withdrawal Selected`\n\nkeeps the account and amount. Together, the current state and its stored values tell the ATM what has already happened and what it can do next.\n\nNow let’s see what this looks like in async Rust. We will use a small function that calculates the total price of two items in a shopping cart. The prices do not arrive immediately, so the function must get the socks price, then the shoes price, and finally add them together.\n\n``` php\nasync fn cart_total(socks: Receiver<i64>, shoes: Receiver<i64>) -> i64 {    let socks_price = socks.await;    let shoes_price = shoes.await;    socks_price + shoes_price}\n```\n\n`socks`\n\nand `shoes`\n\nare two slow price lookups. Each one is a `Receiver<i64>`\n\n, the same kind of oneshot receiver we built in Chapter 1, now made generic so it can also carry a number instead of only a string (see chapter 2 of the [repo](https://github.com/jamesfebin/ImpatientProgrammerAsyncRust) for the updated code).\n\nThe function does three things:\n\n- Wait for the socks price.\n- Wait for the shoes price.\n- Add the two prices.\n\nNow slow the function down in your head.\n\nThe first time this future is polled, the socks price may not be ready yet. In that case, it returns `Pending`\n\n.\n\nLater, a worker thread sends the socks price. The waker fires. Runtime polls the future again.\n\nThis time `socks.await`\n\nfinishes and gives us:\n\n``` js\nlet socks_price = 12;\n```\n\nBut we still cannot return the total. We have not loaded the shoes price yet.\n\nSo we move to the second await:\n\n``` js\nlet shoes_price = shoes.await;\n```\n\nAnd if the shoes price is not ready, the future returns `Pending`\n\nagain.\n\nHere is the question: **While the future is Pending on shoes.await, where is socks_price?**\n\nIt cannot be on the normal call stack in the usual way, because the function stopped and returned `Pending`\n\nto its caller. It also cannot be thrown away, because the final line still needs it:\n\n```\nsocks_price + shoes_price\n```\n\nSo it must live inside the future itself. More specifically, it becomes part of the state machine the compiler builds for this `async fn`\n\n. The future stores which state it is in and the values needed to continue from that state.\n\nWe will turn this async function into a state machine by hand, Rust compiler performs similar transformation behind the scenes.\n\n## Defining The States\n\nHow do we define the possible states of this state machine?\n\nWe look for the places where the work can stop and later continue. In an `async fn`\n\n, those places are the `.await`\n\npoints.\n\nAnd after both waits finish, the future still needs a final state that says the work is over. Once the total is returned the future is done, and a state machine needs an explicit finished state so a future that has already returned `Ready`\n\nis never accidentally polled forward again.\n\nNow notice two things these three states share. The future is always in exactly one of them, never two at once. And each one has to remember different values to continue from where it paused.\n\nRust has a type built for exactly that use case. An `enum`\n\nis a value that is always exactly one of a fixed set of named variants. And in Rust a variant is not just a bare label: each one can store its own data, and different variants can hold different fields, even of different types. That is precisely what we need, one variant per state, holding the values that state must remember.\n\nSo we turn each state into a variant and store inside it only the values that state still needs. We will call them `Start`\n\n(still waiting on the socks price), `GotSocks`\n\n(socks price in, shoes price not yet), and `Done`\n\n(the total has been returned):\n\n```\npub enum CartTotal {    Start {        socks: Receiver<i64>,        shoes: Receiver<i64>,    },    GotSocks {        socks_price: i64,        shoes: Receiver<i64>,    },    Done,}\n```\n\nThis type change follows the code `let socks_price = socks.await;`\n\n. The moment we await `socks`\n\n, the receiver is used up. What we keep is the price it returned, a plain number. So the next state no longer stores `socks`\n\n; it stores `socks_price`\n\n.\n\n**Why don’t we capture shoes_price and the function return value in the state machine?**\n\nBecause nothing pauses after them. A value only needs a home in the state machine if it has to survive an `.await`\n\n. `socks_price`\n\ndoes: it is created at the first `.await`\n\nand still needed after the second, so it has to wait inside `GotSocks`\n\n. `shoes_price`\n\nis created at the last `.await`\n\n, and nothing pauses after that. The function runs straight on to add the two prices and return, all in a single poll. The total is handed back the instant it is computed. Neither value ever has to be remembered between polls, so neither is stored, and the machine simply lands in `Done`\n\n.\n\n## Build The State Machine\n\nLet’s implement it. Clone [this repo](https://github.com/jamesfebin/ImpatientProgrammerAsyncRust) and use the starter project in the `chapter2`\n\nfolder. It has a few small changes from Chapter 1 code, the oneshot channel is now generic, and the project structure has been updated.\n\nCreate the file `state_machine.rs`\n\ninside `tinyrt/src`\n\nfolder. We start with the imports and the states we defined with the enum.\n\n```\nuse std::future::Future;use std::pin::Pin;use std::task::{Context, Poll};use crate::oneshot::Receiver;pub enum CartTotal {    Start {        socks: Receiver<i64>,        shoes: Receiver<i64>,    },    GotSocks {        socks_price: i64,        shoes: Receiver<i64>,    },    Done,}\n```\n\nThen add the constructor that puts the machine in its first state:\n\n``` php\nimpl CartTotal {    pub fn new(socks: Receiver<i64>, shoes: Receiver<i64>) -> Self {        CartTotal::Start { socks, shoes }    }}\n```\n\nThe enum describes what the future can store. The `poll`\n\nmethod describes how the future moves from one state to the next.\n\nInside `poll`\n\n, we need to inspect and update the state machine. So we need mutable access to the enum value itself. `self.get_mut()`\n\ngives us that. This is enough for the simple future we are building here. Later, we will see that some futures need extra protection before Rust lets us move or access their stored state. That is the problem `Pin`\n\nis designed to handle, and we will come back to it in a later chapter.\n\nThen we enter a loop. The match tells us which state the future is currently in: `Start`\n\n, `GotSocks`\n\n, or `Done`\n\n. The code for that state decides what happens next.\n\nIf the state is waiting on something that is not ready, `poll`\n\nreturns `Pending`\n\n. If the state can move forward, we replace the enum with the next state and let the loop check again. That is how one call to `poll`\n\ncan move through more than one state when the values are already ready.\n\n```\nimpl Future for CartTotal {+CartTotal can now be polled like any other future    type Output = i64;+when it finishes, it returns an i64    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<i64> {        let this = self.get_mut();+get mutable access to the enum value        loop {+keep moving while progress is possible            match this {+check the current state: Start, GotSocks, or Done                // state arms go here+each arm decides whether to wait, move, or finish            }        }    }}\n```\n\nIn `Start`\n\n, the future is sitting at the first `.await`\n\n:\n\n``` js\nlet socks_price = socks.await;\n```\n\nSo the first thing `poll`\n\nmust do is poll the socks receiver.\n\nIf socks are not ready, the cart total is not ready either. We stay in `Start`\n\nand return `Pending`\n\n.\n\nIf socks are ready, we move to `GotSocks`\n\n. That next state stores the socks price and keeps the shoes receiver for the second `.await`\n\n.\n\n```\nloop {+context: the state arms live inside this loop    match this {+context: this match chooses the current state        CartTotal::Start { socks, .. } => {+first state: we are waiting for socks            match Pin::new(socks).poll(cx) {                Poll::Pending => {+socks are not ready, so the cart future waits too                    println!(\"[cart] Start: socks price not back yet, waiting\");                    return Poll::Pending;                }                Poll::Ready(socks_price) => {+socks are ready, so we can move to the next state                    println!(                        \"[cart] Start -> GotSocks: socks are ${socks_price}\"                    );                    let CartTotal::Start { shoes, .. } =+take shoes by field name from the old Start; .. ignores socks                        std::mem::replace(this, CartTotal::Done)+put temporary Done in this, and get the old state back                    else {                        unreachable!()                    };                    *this = CartTotal::GotSocks { socks_price, shoes };+write the real next state                }            }        }    }}\n```\n\nTo move from `Start`\n\nto `GotSocks`\n\n, we need to carry the shoes receiver forward. The next state must contain it.\n\nBut `shoes`\n\ncurrently lives inside `CartTotal::Start`\n\n. To put it into `GotSocks`\n\n, we must take ownership of it.\n\nThat creates a small Rust problem: we cannot pull `shoes`\n\nout and leave the enum half-empty.\n\nThe workaround is to swap the whole state out first.\n\n`std::mem::replace(this, CartTotal::Done)`\n\nputs a temporary `Done`\n\nstate into `this`\n\n. That keeps the state machine holding a complete value. At the same time, it hands back the old state that used to be there.\n\nIn this branch, that old state is the `Start`\n\nstate. So we can take it apart and keep only the `shoes`\n\nreceiver:\n\n``` js\nlet CartTotal::Start { shoes, .. } =    std::mem::replace(this, CartTotal::Done)else {    unreachable!()};\n```\n\nThe pattern uses the field name `shoes`\n\n, so the order of fields does not matter. The `..`\n\nmeans “ignore the rest,” which includes `socks`\n\n.\n\nNow that we own `shoes`\n\n, we can write the real next state:\n\n```\n*this = CartTotal::GotSocks { socks_price, shoes };\n```\n\nThat is the first state transition. The function has crossed the first `.await`\n\n, so `socks_price`\n\nis no longer just a temporary local. It is now saved inside the `GotSocks`\n\nstate while the future waits for shoes.\n\nNext comes the second state.\n\nIn `GotSocks`\n\n, the socks price is already stored, so we poll the shoes receiver. If shoes are not ready, we return `Pending`\n\nand keep waiting in `GotSocks`\n\n. If shoes are ready, we can add the two prices, move to `Done`\n\n, and return the total.\n\n```\nloop {+context: still inside the same poll loop    match this {+context: this is another arm of the same state match        // CartTotal::Start { ... }+previous state arm goes above this one        CartTotal::GotSocks { socks_price, shoes } => {+second state: socks are already saved            match Pin::new(shoes).poll(cx) {                Poll::Pending => {+shoes are not ready, so keep waiting in GotSocks                    println!(                        \"[cart] GotSocks: shoes price not back yet, ${socks_price} waits as a field\"                    );                    return Poll::Pending;                }                Poll::Ready(shoes_price) => {+shoes are ready, so the total can be computed                    let total = *socks_price + shoes_price;                    println!(                        \"[cart] GotSocks -> Done: shoes are ${shoes_price}, cart total = ${total}\"                    );                    *this = CartTotal::Done;+mark the future as finished                    return Poll::Ready(total);+hand the final answer back to the executor                }            }        }    }}\n```\n\nThat leaves the terminal state.\n\n`Done`\n\nexists so the future remembers that it has already returned its answer. Once a future has returned `Ready`\n\n, it should not run its body again.\n\n``` js\nCartTotal::Done => {    panic!(\"polled CartTotal after it already returned the total\")}\n```\n\nHere’s the full implementation of the state machine.\n\n```\nimpl Future for CartTotal {    type Output = i64;    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<i64> {        let this = self.get_mut();        loop {            match this {                CartTotal::Start { socks, .. } => {                    match Pin::new(socks).poll(cx) {                        Poll::Pending => {                            println!(\"[cart] Start: socks price not back yet, waiting\");                            return Poll::Pending;                        }                        Poll::Ready(socks_price) => {                            println!(                                \"[cart] Start -> GotSocks: socks are ${socks_price}\"                            );                            let CartTotal::Start { shoes, .. } =                                std::mem::replace(this, CartTotal::Done)                            else {                                unreachable!()                            };                            *this = CartTotal::GotSocks { socks_price, shoes };                        }                    }                },                CartTotal::GotSocks { socks_price, shoes } => {                    match Pin::new(shoes).poll(cx) {                        Poll::Pending => {                            println!(                                \"[cart] GotSocks: shoes price not back yet, ${socks_price} waits as a field\"                            );                            return Poll::Pending;                        }                        Poll::Ready(shoes_price) => {                            let total = *socks_price + shoes_price;                            println!(                                \"[cart] GotSocks -> Done: shoes are ${shoes_price}, cart total = ${total}\"                            );                            *this = CartTotal::Done;                            return Poll::Ready(total);                        }                    }                },                CartTotal::Done => {                    panic!(\"polled CartTotal after it already returned the total\")                }            }        }    }}\n```\n\nAlso, expose the new module from `lib.rs`\n\n:\n\n```\npub mod block_on;pub mod oneshot;pub use block_on::block_on;pub mod state_machine;\n```\n\n## Run The State Machine\n\nNow we need two slow price lookups.\n\nWe will use the oneshot channel from Chapter 1 for this. Each lookup creates a `Sender`\n\nand a `Receiver`\n\n. The sender runs on a worker thread and sends the price later. The receiver is the future that `CartTotal`\n\npolls.\n\nThe first sender sends the socks price after 200ms. The second sender sends the shoes price after 400ms. Until those values arrive, the receivers return `Pending`\n\n, which gives the hand-written state machine a real chance to pause and resume.\n\nCreate `ch02_state_machine.rs`\n\ninside `tinyrt/examples`\n\nfolder.\n\n```\nuse std::thread;use std::time::Duration;use tinyrt::block_on;use tinyrt::state_machine::CartTotal;use tinyrt::oneshot::{self, Receiver};Two delayed pricesEach sender runs on a separate thread and sends its price later.fn price_lookups() -> (Receiver<i64>, Receiver<i64>) {    let (socks_tx, socks_rx) = oneshot::channel::<i64>();    let (shoes_tx, shoes_rx) = oneshot::channel::<i64>();    thread::spawn(move || {        thread::sleep(Duration::from_millis(200));+socks become ready first        socks_tx.send(12);+wakes the socks receiver    });    thread::spawn(move || {        thread::sleep(Duration::from_millis(400));+shoes become ready later        shoes_tx.send(89);+wakes the shoes receiver    });    (socks_rx, shoes_rx)+return the receiving ends to CartTotal}\n```\n\nNow drive `CartTotal`\n\nwith `block_on`\n\n.\n\n``` js\nfn main() {    let (socks, shoes) = price_lookups();    let total = block_on(CartTotal::new(socks, shoes));+poll the state machine until it returns Ready    println!(\"cart total = ${total}\");}\n```\n\nRun it:\n\n```\ncargo run --example ch02_state_machine\n```\n\nThe hand-written machine prints every step:\n\n``` php\n[cart] Start: socks price not back yet, waiting[cart] Start -> GotSocks: socks are $12[cart] GotSocks: shoes price not back yet, $12 waits as a field[cart] GotSocks -> Done: shoes are $89, cart total = $101cart total = $101\n```\n\nThat is the state machine running.\n\nWhen you write the original `async fn`\n\n, you do not write this enum or this `poll`\n\nmethod yourself. The compiler builds a state machine with a similar shape behind the scenes.\n\n## Applying The State Machine Model\n\nNow that we have built the state machine by hand, we can use it to reason through common async issues.\n\nThe sections below are the same state-machine idea applied to real problems: locks held while waiting, memory kept alive by parked tasks, executor threads blocked by long `poll`\n\ncalls, and compiler errors that point at `.await`\n\nbut are really about what the future stored.\n\n### Reason About Locks Held While Waiting\n\nShared state is common in async programs: a counter, a cache, a connection table, or some small piece of application state that many tasks need to update. A `Mutex`\n\nprotects that state so only one task can change it at a time.\n\nWhen you call `state.lock()`\n\n, Rust gives you a guard. The guard is the permission to access the protected value, and the mutex stays locked for as long as that guard exists. When the guard is dropped, the lock is released.\n\nIn this example, `touch_db()`\n\nstands in for an async database call or network call. It may return `Pending`\n\n, so the task may pause there.\n\nThis code looks ordinary:\n\n``` js\nasync fn bump(state: Arc<Mutex<i32>>) {    let mut guard = state.lock().unwrap();    touch_db().await;    *guard += 1;}\n```\n\nBut when this future is used on a multi-threaded async runtime, the compiler can reject it with an error like this:\n\n```\nfuture cannot be sent between threads safelyfuture is not Send as this value is used across an awaitthe trait Send is not implemented for MutexGuard<'_, i32>\n```\n\nThe state-machine model explains why.\n\n`guard`\n\nis created before the `.await`\n\n, and it is used after the `.await`\n\n. That means `guard`\n\nmust survive the pause. So the suspended state has to store the `MutexGuard`\n\n.\n\nThat has two consequences.\n\nFirst, the lock stays held while the task is waiting for `touch_db()`\n\n. The task is not actively using the locked data during that wait, but other tasks still cannot take the lock. In a busy server, that can create long waits, poor throughput, or deadlocks that are hard to understand from the surface code.\n\nSecond, if a task pauses on one worker thread, the runtime may resume it later on another worker thread.\n\nFor that to be safe, the future must be safe to move between threads. That is what `Send`\n\nmeans here: a value can be transferred to another thread without breaking Rust’s safety rules.\n\nA future is `Send`\n\nonly if the values stored inside it are `Send`\n\n. `std::sync::MutexGuard`\n\nis not `Send`\n\n, so the whole future is not `Send`\n\n, and Rust rejects using it in a place that requires a thread-safe future.\n\nThe usual fix is to finish the locked work before the `.await`\n\n:\n\n``` js\nasync fn bump(state: Arc<Mutex<i32>>) {    {        let mut guard = state.lock().unwrap();        *guard += 1;    } // guard dropped here    touch_db().await;}\n```\n\nNow the paused state does not store the guard. The lock is released before the task waits, and the future no longer carries a non `Send`\n\nguard across `.await`\n\n.\n\nWhen the lock truly must be held across an `.await`\n\n, use an async-aware lock such as `tokio::sync::Mutex`\n\n. Its guard is designed to cross `.await`\n\npoints, and waiting for the lock does not block an executor thread. But the lock is still held while the task is paused.\n\n### Reason About Memory Held While Waiting\n\nThe same rule applies to ordinary data too. If a value is still needed after `.await`\n\n, the paused future must keep it.\n\nWith a lock guard, that meant the lock stayed held. With a large value, the consequence is memory: the value stays alive while the task waits.\n\nConsider the following example, where the task creates a large buffer, waits on the network, and later uses only the buffer length:\n\n``` js\nasync fn example() {    let big_buffer = vec![0_u8; 1024 * 1024];    wait_for_network().await;    println!(\"{}\", big_buffer.len());}\n```\n\n`big_buffer`\n\nis used after the `.await`\n\n, so the future must keep it while `wait_for_network()`\n\nis pending. Here `big_buffer`\n\nstands in for something you actually produced, like a decoded image, a parsed request body, or a file you read into memory, and `big_buffer.len()`\n\nstands in for the small fact you needed from it.\n\nFor one task, this may not matter. But async programs often have many tasks paused at the same time: many requests, many connections, many jobs, or many timers. If each paused task keeps a large buffer alive across `.await`\n\n, memory usage grows with the number of paused tasks.\n\nThe future only stores the `Vec`\n\nhandle, a few bytes, but that handle keeps the megabyte on the heap alive. So even while the task is just waiting on the network, the buffer cannot be freed. A value stored inline instead, like a big array or struct, grows the future itself. Either way, every waiting task has its future parked somewhere.\n\nThe numbers here are kept deliberately clean so the pattern is easy to see. Each one on its own is realistic: 10,000 tasks waiting at once is ordinary for a busy server, and 1 MB buffers are common once you are decoding images or buffering response bodies. What makes this a worst case is the combination, every task holding a full buffer and all of them parked at the same moment. Real workloads are lumpier. Buffer sizes vary, and tasks finish and free memory continuously. The point is not the exact 10 GB. The point is that the cost is paid per paused task, so it scales with how many futures are parked at the same time.\n\nSo the question is **Does the whole buffer need to stay alive while this future waits?**\n\nIf the code really needs the buffer after the `.await`\n\n, keeping it can be correct. But if you only need a small piece of information from it, extract that piece before the `.await`\n\nand let the large value drop.\n\n``` js\nasync fn better() {    let len = {        let big_buffer = vec![0_u8; 1024 * 1024];        big_buffer.len()    };    wait_for_network().await;    println!(\"{len}\");}\n```\n\nNow the paused state only needs to store `len`\n\n, not the large buffer.\n\n### Reason About Performance\n\nAsync does not make every line non-blocking.\n\nA future gives the executor control back only when `poll`\n\nreturns `Pending`\n\nor `Ready`\n\n. Code between two `.await`\n\npoints runs like normal synchronous Rust inside one poll, so `.await`\n\nis where async code gets a chance to pause and let the executor run something else.\n\nBlocking code does not do that. It keeps running inside the current `poll`\n\ncall.\n\n```\nasync fn bad_task() {    std::thread::sleep(Duration::from_secs(5));}\n```\n\nEven though this function is `async`\n\n, there is no real async pause inside it. The generated future cannot return `Pending`\n\nduring those five seconds. The executor only sees one long `poll`\n\ncall that does not return.\n\nFor waiting on time in async code, use an async timer such as `tokio::time::sleep`\n\n. It can return `Pending`\n\n, so the executor can run other tasks while the timer is waiting.\n\nThe same problem appears with CPU-heavy work:\n\n```\nasync fn handle() {    parse_large_json();    compress_payload();    socket.write_all(...).await;}\n```\n\nThe solution is to keep the synchronous chunks between `.await`\n\npoints small and fast. If you must call blocking code, move it out of the async worker path with `tokio::task::spawn_blocking`\n\nor a separate blocking thread pool. For sustained CPU-heavy work, prefer a dedicated CPU pool or Rayon, so compute-heavy jobs do not saturate threads meant for short blocking calls.\n\n### Reason About Compiler Errors\n\nWhen async Rust gives a confusing compiler error, the useful question is usually not “what is wrong with this line?” but “what did this future capture or keep alive?”\n\nWe already saw one example with `MutexGuard`\n\n: the guard lived across `.await`\n\n. Another common version is a borrowed value that may not live long enough.\n\nFor example:\n\n```\nasync fn handle(client: &Client) {    tokio::spawn(async {        client.request().await;    });}\n```\n\n`client: &Client`\n\nmeans `handle`\n\ndoes not own the client. It only borrowed access to a client owned somewhere else. That borrowed access is only guaranteed to be valid while `handle`\n\nis running.\n\nBut the async block creates another future. If that future is handed to the runtime, the runtime may keep it and poll it later, even after `handle`\n\nhas returned. If the future stored `client: &Client`\n\n, it would be holding borrowed access to something that may no longer be available.\n\nThe error is not really about the call to `client.request()`\n\n. It is about the future storing a borrowed reference.\n\nThat is why async runtimes often require spawned futures to satisfy bounds like:\n\n```\nFuture + Send + 'static\n```\n\nIn this context: Send means the future can move between worker threads safely. ‘static means the future does not depend on borrowed data that may disappear when the current function returns.\n\nA common fix is to move owned data into the task:\n\n```\nasync fn handle(client: Arc<Client>) {\n    tokio::spawn(async move {\n        client.request().await;\n    });\n}\n```\n\nNow the future stores an owned `Arc<Client>`\n\n, not a borrowed `&Client`\n\n. The `Arc`\n\nkeeps the client alive as long as the task needs it.\n\nWhen you hit async compiler errors, here are some questions to consider:\n\n- What does this async block capture?\n- Which locals are still alive across .await?\n- Could this future be kept after this function returns?\n- Could this future move to another worker thread?\n\nThere is much more to cover in this area. But it need concepts we have not introduced yet, especially `Pin`\n\n, tasks, executors, and how a runtime schedules work.\n\nIn the next chapters, we will keep building on this same state machine idea. First we will see why `Pin`\n\nexists, then build the task and executor that schedule futures, then connect those pieces to Tokio, `spawn`\n\n, `Send + 'static`\n\n, real I/O wakeups, timers, channels, and the patterns used in async services.", "url": "https://wpnews.pro/news/reasoning-about-async-rust-with-state-machines", "canonical_source": "https://aibodh.com/posts/async-rust-chapter-2-what-async-fn-compiles-into/", "published_at": "2026-06-30 13:12:17+00:00", "updated_at": "2026-06-30 13:20:59.034013+00:00", "lang": "en", "topics": ["large-language-models"], "entities": ["Rust", "Discord"], "alternates": {"html": "https://wpnews.pro/news/reasoning-about-async-rust-with-state-machines", "markdown": "https://wpnews.pro/news/reasoning-about-async-rust-with-state-machines.md", "text": "https://wpnews.pro/news/reasoning-about-async-rust-with-state-machines.txt", "jsonld": "https://wpnews.pro/news/reasoning-about-async-rust-with-state-machines.jsonld"}}