TL;DR:
tokio::select!polls multiple async branches concurrently and executes the first one that becomes ready, cancelling the rest. It is the primary tool for racing futures, implementing timeouts, and responding to shutdown signals. Each branch is a pattern + async expression pair. If multiple branches are ready simultaneously,select!picks one at random (to avoid starvation). Be aware of cancellation safety: operations dropped mid-flight may lose data.
What Is tokio::select!?
tokio::select! is a macro that polls multiple async expressions at once and runs whichever branch completes first.
use tokio::time::{sleep, Duration};
async fn fetch_with_timeout(url: &str) -> Result<String, &'static str> {
tokio::select! {
result = fetch(url) => result.map_err(|_| "fetch failed"),
_ = sleep(Duration::from_secs(5)) => Err("timed out"),
}
}The sleep branch and the fetch branch race. Whichever resolves first wins; the other is dropped.
How Do You Handle Shutdown Signals?
select! is the standard way to listen for a cancellation token or shutdown channel alongside normal work.
use tokio::sync::oneshot;
async fn worker(mut shutdown: oneshot::Receiver<()>) {
loop {
tokio::select! {
_ = do_work() => {},
_ = &mut shutdown => {
println!("shutting down");
break;
}
}
}
}This pattern appears in every production Tokio service: work loop + shutdown receiver in select!.
What Is Cancellation Safety?
A future is cancellation-safe if dropping it mid-poll loses no data. select! cancels losing branches by dropping them.
// tokio::sync::mpsc::Receiver::recv() IS cancellation-safe
// std::io::AsyncReadExt::read_exact() is NOT; partial reads are lost
let mut buf = [0u8; 1024];
tokio::select! {
n = socket.read(&mut buf) => { /* safe */ }
_ = shutdown.recv() => {}
}Check the Tokio docs for each async fn's cancellation safety note. For non-safe operations, buffer the result externally before the select!.
How Does select! Differ From join!?
select! runs the first-to-finish branch. join! waits for all branches to finish.
select! | join! | |
|---|---|---|
| Completes when | First branch ready | All branches done |
| Other branches | Cancelled (dropped) | All run to completion |
| Use for | Timeouts, racing, shutdown | Parallel independent work |
| Returns | One value | Tuple of all values |
Use join! when you need all results. Use select! when you need the fastest result or want to cancel on a signal.
Frequently Asked Questions
Yes, but pin it first. Use tokio::pin!(fut) and reference it as &mut fut in each select! to avoid moving it.
select! suspends the task until at least one branch makes progress; it does not spin.
Yes. A else branch runs immediately if all branches are ready to be polled but none would block; rarely needed in practice.
Sources
Related Glossary Terms
- tokio: The async runtime
select!runs inside - spawn: Alternative for concurrent work that runs to completion
- channel: Often used with
select!for message passing - future: What each
select!branch polls
Keep Reading
- Rust Async/Await Explained: select! is the core concurrency primitive once you understand async/await
