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!.
Recipe
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.
Working Example
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 unlessbiased;omitted).timeoutwraps any future with a deadline.- Remaining branches are cancelled when
select!resolves.
Deep Dive
How It Works
join!: Waits for all branches; tuple of results.try_join!: Short-circuits on firstErr(Result futures).select!: First branch wins; others dropped unlessselect!loop.FuturesUnordered: Dynamic set of futures completing in finish order.
Macro Comparison
| 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 |
Rust Notes
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!requiresasynccontext and Tokio runtime for timer branches.- Fairness: use random
select!patterns or shuffle for unbiased choice in rare cases. - In loops,
select!is the idiomatic event multiplexer.
Gotchas
- select! in a tight loop without cancel-safe branches - May lose events. Fix: Use
biasedcarefully; read Tokio cancel safety docs per method. - join! with dependent futures - Second needs output of first - not independent. Fix: Sequential
awaitorthen. - Mutex guard across select branches - Not
Send. Fix: Scope locks outsideselect!. - Biased select starvation - First branch always ready starves others. Fix: Reorder branches or use fair polling patterns.
- join_all unbounded - Spawns N concurrent futures at once. Fix:
buffer_unorderedor semaphore-limited batching.
Alternatives
| 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 |
FAQs
join! vs spawn?
join! runs futures on the current task concurrently (cooperative). spawn creates separate tasks on the runtime.
What is cancel safety?
A future is cancel-safe if dropping it at an await point loses no required data - critical for select! loops.
try_join! with different errors?
Use Result with common error type or map errors before join.
select! with channels?
Common pattern: recv() branches for shutdown, work, and timers in server loops.
race without select?
futures::future::select takes two futures - less ergonomic than macro for many branches.
timeout vs sleep in select?
timeout wraps one future; select! can race arbitrary branches including timers.
JoinHandle in select?
Await JoinHandle as a branch to race task completion against shutdown signal.
Streams and select?
tokio::select! supports some_stream.next() => for multiplexed async iteration.
Testing joins?
Use tokio::time::pause and advance to control virtual time in tests.
Axum graceful shutdown?
select! between server accept loop and ctrl_c() signal - see Graceful Shutdown page.
Related
- Async Basics - first join
- select! & Concurrency - Tokio select patterns
- Cancellation & Timeouts - deadlines
- Tasks & JoinHandles - spawned joins
- Graceful Shutdown - shutdown select
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+.