A handle to a value that has not arrived
Guides 1 and 2 gave you the machinery underneath: a thread pool of worker threads pulling units of work off a task queue, and a work-stealing scheduler that keeps those workers busy by letting an idle one steal from a neighbour. What you submitted there was a fire-and-forget task — you handed in a closure and the pool ran it. But most real work produces a result you need back: the bytes a network read returns, the row a database query finds, the number a long computation lands on. The question this guide answers is the missing half: when you submit work, how do you get its result without blocking the thread that asked?
The answer is a future: a small handle object you receive immediately, standing in for a value that will exist later. Think of it as the paper ticket you get at a coat check. The ticket is not your coat — but it is a genuine claim on it, and you can walk away, do other things, and redeem it when the coat is ready. A future has exactly two phases. While the work runs it is pending: asking it for the value would mean waiting. Once the work finishes it becomes ready (or resolved), and the future now holds either the result or, just as honestly, the error that occurred — because real work fails, and a future that could only ever carry success would be a lie.
Notice what the future bought you: decoupling in time. Submitting the work and consuming its result are now two separate moments, and the thread that submitted is free in between. That is the whole point. A future does not make the work faster; it makes the waiting productive, because you hold a cheap handle instead of a blocked thread. This is the same lesson as the earlier non-blocking-I/O rung — never let one slow operation pin a whole thread — now lifted to the level of arbitrary computations, not just file descriptors.
Two ends of one channel: the promise fills, the future reads
Where does the value actually come from? Here is the cleanest way to picture it, and it explains why the pair is so often called futures and promises. They are the two ends of a single one-shot channel. The promise is the write end, held by whoever is computing the value; calling its `set_value(...)` (or `set_exception(...)`) fulfils it exactly once. The future is the read end, handed to whoever is waiting; it observes the value the promise placed. One producer, one consumer, one value — and crucially the order does not matter: the consumer can grab the future and wait before the producer has computed anything, or the producer can fulfil the promise before the consumer ever looks. The channel remembers.
Thread A (producer) Thread B (consumer)
------------------- -------------------
promise<int> p; future<int> f = p.get_future();
future<int> f = p.get_future(); // ... do other work ...
submit_to_pool([&]{ int v = f.get(); // blocks ONLY
int r = slow_compute(); // if not ready yet;
p.set_value(r); // fulfil once // returns r once set
});That last point is not a detail — it is the contract that makes futures safe. The pair carries a happens-before edge from the memory-model rung: everything the producer wrote before `set_value` is guaranteed visible to the consumer after `get` returns, with no data race and no extra lock on your part. The shared state behind the pair holds three things: the value-or-error slot, a ready flag, and the synchronization to publish one to the other. So a future is not just a convenience wrapper; it is a tiny, correct hand-off of ownership of a result from one thread to another, with the ordering already proven. You are reusing the hard-won machinery of acquire/release without having to spell it out each time.
Continuations: don't block — say what to do next
The naive way to use a future is to call `get()` and block until it is ready. That works, but it throws away the whole advantage: a blocked thread is a parked thread, and if it is one of your precious pool workers from guide 1, you have just removed a worker from the pool to do nothing but wait. On a small pool this is how a program deadlocks itself — every worker sits blocked on a future whose fulfilling task is still stuck in the queue with no worker free to run it. Blocking on a future inside the executor is the cardinal sin: don't block the executor.
The fix is to stop asking 'is it ready yet?' and instead register what should happen when it becomes ready. That registered piece of work is a continuation — literally, the continuation of your computation, the code that picks up once the value lands. Instead of `int v = f.get(); use(v);` you write something like `f.then(use);`: 'when f resolves, hand its value to `use` and run that on a pool thread.' You return immediately; the thread is never blocked. When the promise fulfils the future, the runtime schedules the continuation onto the pool as a fresh task — which is exactly the task model from guide 1, now generated automatically at the moment a value becomes ready.
Async/await: a state machine that reads like a straight line
Here is the trick that made all of this practical. Async/await is syntax that lets you write continuation-passing code as if it were ordinary top-to-bottom code, and lets the compiler do the fragmenting for you. You mark a function `async`; inside it you `await` a future. The `await` means: 'if this value is not ready, suspend right here and hand control back to the runtime; resume from this exact spot once it is ready.' The crucial word is suspend, not block. The thread is not parked — it is released to run other tasks. Only this logical flow pauses.
// What you WRITE (reads like a straight line):
async fn fetch_user(id) {
let conn = await open_db(); // suspend point 1
let row = await conn.query(id); // suspend point 2
return parse(row);
}
// What the compiler GENERATES (a state machine):
// state 0: start open_db; on ready -> save conn, go to state 1
// state 1: start query; on ready -> save row, go to state 2
// state 2: return parse(row) (done)
// Locals conn/row live in a struct, NOT on a thread's stack.Look at what the compiler did. It cut the function at each `await` and turned it into a state machine: a struct holding a 'which step am I on' tag plus exactly the local variables that must survive across a suspension. Resuming the function is just a jump to the right case based on the tag. Each `await` is precisely a continuation — the same mechanism from the previous section — but now generated and wired by the compiler instead of typed by you. The code you read is a straight line; the code that runs is a resumable state machine. That gap, bridged invisibly, is the whole gift of async/await: continuation-passing performance with sequential-code readability.
Because the surviving locals live in that heap struct and not on a thread's call stack, this flavour is called stackless — the suspended task carries no machine stack of its own, only its tiny state struct. That is why an async task can be tens of bytes while an OS thread costs a stack measured in KiB or MiB, and why a single machine runs millions of async tasks but only thousands of threads. The trade-off, which guide 4 unfolds in full, is that a stackless `await` can only suspend at points the compiler transformed — you cannot pause in the middle of an ordinary called function the way a stackful coroutine can. We are naming that seam now so the next guide can pry it open.
Who runs the state machine? The executor and its rules
A state machine that nobody steps is just a frozen struct. Something must call 'resume' on a task whenever its awaited value becomes ready, and that something is the executor (or runtime) — and here the loop closes back to where we started. The executor is the thread pool and work-stealing scheduler of guides 1 and 2, now driving suspendable tasks instead of run-to-completion closures. A handful of OS threads each pull ready tasks from a queue and resume them; a task runs until its next `await`, suspends, and the thread immediately grabs the next ready task. This is m-to-n scheduling: m tiny async tasks multiplexed over n OS threads, the entire reason a few cores can serve a flood of concurrent work.
This is cooperative, not preemptive, scheduling, and that single fact governs how you must write async code. A task keeps a thread until it voluntarily yields at an `await`. So if one task does a long CPU loop, or — the classic disaster — calls a blocking operation like a synchronous `read()` or `pthread_mutex_lock()` with no `await`, it never yields, and it monopolises that thread. With only n threads, a few such tasks freeze the entire runtime: this is don't block the executor again, and it is the single most common way async programs die. The rule is sharp: inside async code, every potentially-slow operation must be its async, awaitable form, so the thread is freed on every wait.
Where this leaves you
Step back and the layers stack cleanly. A future is a handle to a not-yet value, with a promise as its write end and a happens-before guarantee baked in. A continuation says what to do when the value lands, turning blocking into scheduling. Async/await is sugar that writes those continuations for you as a stackless state machine, so suspendable code reads like a straight line. And the executor — your thread pool from guide 1 — is what resumes those machines, under cooperative rules you must respect. Each layer reuses the one below; nothing here is new machinery, only new shape over the machinery you already built.
Two honest gaps remain, and the rest of the rung exists to close them. First, we leaned on the word stackless — guide 4 contrasts it head-on with stackful coroutines, which carry a real stack and can suspend anywhere, trading more memory per task for fewer restrictions on where you pause. Second, we taught how to start and await work, but said nothing about how a group of async tasks should be owned, cancelled, and cleaned up together — and an async task that leaks, or that you can never cancel, is a real bug, not a tidy abstraction. That is structured concurrency, the subject of guide 5. You now have the engine; the next two guides give it shape and brakes.