If you've written async Rust, you've seen this pattern a thousand times:

async fn fetch_data() -> String {
    let response = client.get(url).await?;
    response.text().await?
}

It looks synchronous. It reads like synchronous code. But it's not. And understanding why it's not — and what actually happens when you hit that .await — is the difference between writing async code that works and writing async code that mysteriously hangs.

The Keyword That Isn't

Here's the thing most Rust developers don't realize: async is not a runtime feature.

In JavaScript, async hooks into the event loop. In Python's asyncio, it interacts with the asyncio event loop. But in Rust? async is pure compiler magic. It transforms your function into a state machine. That's it.

Your async fn fetch_data() becomes a struct with a poll method. The compiler generates a state machine that tracks: "Have I called client.get() yet? Did I get the response? Am I waiting for the body?"

// What the compiler generates (simplified)
enum FetchDataFuture {
    Pending { client: Client, url: String },
    Ready(String),
}

impl Future for FetchDataFuture {
    fn poll(&mut self, cx: &mut Context<'_>) -> Poll<Self::Output> {
        match self {
            FetchDataFuture::Pending { .. } => {
                // Do a little bit of work
                // If not done, return Poll::Pending
            }
            FetchDataFuture::Ready(s) => Poll::Ready(s),
        }
    }
}

The await keyword just means: "Call poll on this future. If it returns Pending, yield control back to whoever called me."

Where Does Control Go?

This is the part that trips people up. When your .await returns Pending, your task gets suspended. The thread doesn't block — it goes back to the executor and does something else.

Think of it like a cooperative multitasking system, not preemptive. Your async code decides when to yield. The .await is the yield point.

async fn example() {
    let a = fetch_a().await;  // Yield here if pending
    let b = fetch_b().await;  // Yield here if pending  
    let c = fetch_c().await;  // Yield here if pending
    
    (a, b, c)
}

If fetch_a() takes 100ms and fetch_b() takes 50ms, the executor can start fetch_b() while waiting for fetch_a(). That's the efficiency gain. But only if you're actually awaiting things concurrently:

// Sequential: takes 150ms
let a = fetch_a().await;
let b = fetch_b().await;

// Concurrent: takes 100ms  
let (a, b) = tokio::join!(fetch_a(), fetch_b());

The Runtime Is a Task Scheduler

Tokio (and other runtimes) is fundamentally a task scheduler. It maintains a queue of suspended futures. When I/O completes (network response, file read), the OS notifies Tokio, and Tokio wakes up the corresponding task by calling its poll method again.

This is why you need a runtime at all. The async keyword gives you the machinery to pause and resume, but something has to actually do the scheduling. That's Tokio's job.

Why This Matters

Understanding this changes how you debug:

The next time you write .await, remember: you're not calling a function. You're asking the executor to poll a future, and if it's not ready, you're voluntarily giving up the thread until something else wakes you up.

That's async Rust in a nutshell. No magic. Just a state machine and a scheduler.