Combining Futures
Independent async work can run concurrently with join!, try_join!, or FuturesUnordered. When you need the first completion or cancellation, use select! or tokio::select!.
Search across all documentation pages
Independent async work can run concurrently with join!, try_join!, or FuturesUnordered. When you need the first completion or cancellation, use select! or tokio::select!.
Quick-reference recipe card - copy-paste ready.
#[tokio::main]
async fn main() {
let (a, b) = tokio::join!(
async { 1 },
async { 2 },
);
println!("{}", a + b);
}When to reach for this: Parallel HTTP fetches, waiting on multiple channels, or racing a timeout against slow I/O.
use tokio::time::{sleep, Duration, timeout};
async fn slow() -> &'static str {
sleep(Duration::from_secs(5)).await;
"late"
}
#[tokio::main]
async fn main() {
tokio::select! {
result = slow() => println!("got {result}"),
_ = sleep(Duration::from_millis(100)) => println!("timeout branch"),
}
match timeout(Duration::from_millis(50), slow()).await {
Ok(v) => println!("{v}"),
Err(_) => println!("timed out"),
}
}What this demonstrates:
select! runs until one branch completes (biased by declaration order unless biased; omitted).timeout wraps any future with a deadline.select! resolves.join!: Waits for all branches; tuple of results.try_join!: Short-circuits on first Err (Result futures).select!: First branch wins; others dropped unless select! loop.FuturesUnordered: Dynamic set of futures completing in finish order.| Tool | Completes when | Cancellation |
|---|---|---|
join! | All done | Drops incomplete if one panics |
select! | First done | Drops other branches |
timeout | Future or deadline | Cancels inner future on timeout |
join_all | All in vec | Collection of results |
use futures::future::join_all;
#[tokio::main]
async fn main() {
let futs = vec![async { 1 }, async { 2 }, async { 3 }];
let results = join_all(futs).await;
println!("{:?}", results);
}tokio::select! requires async context and Tokio runtime for timer branches.select! patterns or shuffle for unbiased choice in rare cases.select! is the idiomatic event multiplexer.biased carefully; read Tokio cancel safety docs per method.await or then.Send. Fix: Scope locks outside select!.buffer_unordered or semaphore-limited batching.
| Alternative | Use When | Don't Use When |
|---|---|---|
Sequential await | Steps depend on prior output | Independent parallel I/O |
spawn + JoinHandle | True concurrent tasks with lifetimes | Simple tuple join on one task |
Channels + select! | Event-driven servers | One-off parallel fetches |
FuturesOrdered | Need results in submission order | First-ready order suffices |
join! runs futures on the current task concurrently (cooperative). spawn creates separate tasks on the runtime.
A future is cancel-safe if dropping it at an await point loses no required data - critical for select! loops.
Use Result with common error type or map errors before join.
Common pattern: recv() branches for shutdown, work, and timers in server loops.
futures::future::select takes two futures - less ergonomic than macro for many branches.
timeout wraps one future; select! can race arbitrary branches including timers.
Await JoinHandle as a branch to race task completion against shutdown signal.
tokio::select! supports some_stream.next() => for multiplexed async iteration.
Use tokio::time::pause and advance to control virtual time in tests.
select! between server accept loop and ctrl_c() signal - see Graceful Shutdown page.
Stack versions: This page was written for Rust 1.97.0 (edition 2024), Tokio 1.x, Axum 0.8, serde 1.0, sqlx 0.8, clap 4, and Polars 0.46+.
Reviewed by Chris St. John·Last updated Jul 19, 2026