What is the difference between futures::future::join_all and futures::future::try_join_all for concurrent future execution?
futures::future::join_all runs all futures to completion regardless of individual failures and returns a Vec of Result values, while futures::future::try_join_all short-circuits on the first error and returns Result<Vec<T>, E>. The key distinction is error propagation semantics: join_all waits for every future to finish and collects all outcomes, making it suitable when you need all results regardless of success or failure. try_join_all stops immediately when any future fails and propagates that error, making it appropriate when all futures must succeed for the overall operation to be considered successful. Both execute futures concurrently, but they handle failures fundamentally differently.
Basic join_all Behavior
use futures::future::join_all;
async fn fetch_urls() -> Vec<Result<String, &'static str>> {
let urls = ["url1", "url2", "url3"];
let futures: Vec<_> = urls.iter().map(|url| async move {
// Simulate some succeeding, some failing
if url.contains("2") {
Err("Failed to fetch")
} else {
Ok(format!("Data from {}", url))
}
}).collect();
// join_all returns Vec<Result<String, &str>>
let results: Vec<Result<String, &str>> = join_all(futures).await;
results
}
#[tokio::main]
async fn main() {
let results = fetch_urls().await;
// All futures completed, results include failures
for result in results {
match result {
Ok(data) => println!("Success: {}", data),
Err(e) => println!("Error: {}", e),
}
}
}join_all always waits for all futures and returns all results.
Basic try_join_all Behavior
use futures::future::try_join_all;
async fn fetch_all_or_fail() -> Result<Vec<String>, &'static str> {
let urls = ["url1", "url2", "url3"];
let futures: Vec<_> = urls.iter().map(|url| async move {
if url.contains("2") {
Err("Failed to fetch")
} else {
Ok(format!("Data from {}", url))
}
}).collect();
// try_join_all returns Result<Vec<String>, &str>
try_join_all(futures).await
}
#[tokio::main]
async fn main() {
match fetch_all_or_fail().await {
Ok(all_data) => {
println!("All succeeded:");
for data in all_data {
println!(" {}", data);
}
}
Err(e) => {
println!("At least one failed: {}", e);
}
}
}try_join_all short-circuits on first error; other futures may still run but the error is returned immediately.
Error Propagation Comparison
use futures::future::{join_all, try_join_all};
use std::time::Duration;
use tokio::time::{sleep, Instant};
async fn task(id: u32, should_fail: bool) -> Result<String, String> {
sleep(Duration::from_millis(100 * id)).await;
if should_fail {
Err(format!("Task {} failed", id))
} else {
Ok(format!("Task {} succeeded", id))
}
}
#[tokio::main]
async fn main() {
// join_all: all tasks complete
let start = Instant::now();
let futures = vec![
task(1, false),
task(2, true), // This fails
task(3, false),
];
let results: Vec<Result<String, String>> = join_all(futures).await;
println!("join_all took: {:?}", start.elapsed());
for r in results {
println!(" {:?}", r);
}
println!();
// try_join_all: short-circuits on first error
let start = Instant::now();
let futures = vec![
task(1, false),
task(2, true), // This fails
task(3, false),
];
let result: Result<Vec<String>, String> = try_join_all(futures).await;
println!("try_join_all took: {:?}", start.elapsed());
match result {
Ok(data) => println!("All succeeded: {:?}", data),
Err(e) => println!("Failed: {}", e),
}
}join_all waits for all tasks; try_join_all returns on first failure.
Type Signature Differences
use futures::future::{join_all, try_join_all};
// join_all signature:
// pub fn join_all<I>(iter: I) -> JoinAll<I::Item>
// where I: IntoIterator, I::Item: Future<Output = T>
// Returns: JoinAll<I::Item> -> Vec<T>
//
// The output is Vec<T>, where T can be anything including Result
// try_join_all signature:
// pub fn try_join_all<I>(iter: I) -> TryJoinAll<I::Item>
// where I: IntoIterator, I::Item: TryFuture
// Returns: TryJoinAll<I::Item> -> Result<Vec<T>, E>
//
// The output is Result<Vec<T>, E>, futures must be TryFuture
#[tokio::main]
async fn main() {
// join_all: futures can return any type
let futures = vec![
async { 1 },
async { 2 },
async { 3 },
];
let results: Vec<i32> = join_all(futures).await;
println!("Numbers: {:?}", results);
// try_join_all: futures must return Result
let try_futures = vec![
async { Ok::<i32, &str>(1) },
async { Ok::<i32, &str>(2) },
async { Ok::<i32, &str>(3) },
];
let result: Result<Vec<i32>, &str> = try_join_all(try_futures).await;
println!("Try result: {:?}", result);
}try_join_all requires TryFuture (returns Result); join_all accepts any Future.
When All Futures Must Succeed
use futures::future::try_join_all;
struct Database;
struct Cache;
struct Storage;
impl Database {
async fn connect(&self) -> Result<(), String> {
Ok(())
}
}
impl Cache {
async fn connect(&self) -> Result<(), String> {
Ok(())
}
}
impl Storage {
async fn connect(&self) -> Result<(), String> {
Err("Storage unavailable".to_string())
}
}
async fn initialize_all_services() -> Result<(), String> {
let db = Database;
let cache = Cache;
let storage = Storage;
// All must succeed for app to start
try_join_all(vec![
db.connect(),
cache.connect(),
storage.connect(),
]).await?;
Ok(())
}
#[tokio::main]
async fn main() {
match initialize_all_services().await {
Ok(()) => println!("All services initialized"),
Err(e) => println!("Startup failed: {}", e),
}
}Use try_join_all when partial success isn't useful.
When You Need All Results Regardless
use futures::future::join_all;
async fn fetch_from_multiple_sources(urls: Vec<&str>) -> Vec<Result<String, String>> {
let futures: Vec<_> = urls.iter().map(|url| async move {
// Each fetch is independent
fetch_url(url).await
}).collect();
join_all(futures).await
}
async fn fetch_url(url: &str) -> Result<String, String> {
// Simulate fetch
if url.contains("error") {
Err(format!("Failed to fetch {}", url))
} else {
Ok(format!("Data from {}", url))
}
}
#[tokio::main]
async fn main() {
let urls = vec!["api1", "api2", "error1", "api3", "error2"];
let results = fetch_from_multiple_sources(urls).await;
// Process successful ones, log failures
for result in results {
match result {
Ok(data) => println!("Got: {}", data),
Err(e) => eprintln!("Failed: {}", e),
}
}
}Use join_all when you want all results, including failures.
Partial Success Handling
use futures::future::join_all;
#[derive(Debug)]
struct ServiceResult {
name: String,
data: Option<String>,
error: Option<String>,
}
async fn check_services() -> Vec<ServiceResult> {
let services = vec!["auth", "database", "cache", "queue"];
let futures: Vec<_> = services.iter().map(|name| async move {
// Simulate health check
if name.contains("queue") {
Err(format!("{} is down", name))
} else {
Ok(format!("{} is healthy", name))
}
}).collect();
let results: Vec<Result<String, String>> = join_all(futures).await;
results.into_iter().enumerate().map(|(i, result)| {
let name = services[i].to_string();
match result {
Ok(data) => ServiceResult { name, data: Some(data), error: None },
Err(e) => ServiceResult { name, data: None, error: Some(e) },
}
}).collect()
}
#[tokio::main]
async fn main() {
let results = check_services().await;
let healthy: Vec<_> = results.iter().filter(|r| r.error.is_none()).collect();
let unhealthy: Vec<_> = results.iter().filter(|r| r.error.is_some()).collect();
println!("Healthy: {:?}", healthy.len());
println!("Unhealthy: {:?}", unhealthy.len());
for result in &unhealthy {
println!("Service {} is down: {:?}", result.name, result.error);
}
}join_all enables partial success patterns.
Empty Collection Behavior
use futures::future::{join_all, try_join_all};
#[tokio::main]
async fn main() {
// join_all with empty collection returns empty Vec
let empty_futures: Vec<async fn() -> i32> = vec![];
let empty_results: Vec<i32> = join_all(empty_futures).await;
println!("join_all empty: {:?}", empty_results); // []
// try_join_all with empty collection returns Ok(Vec::new())
let empty_try_futures: Vec<async fn() -> Result<i32, String>> = vec![];
let empty_result: Result<Vec<i32>, String> = try_join_all(empty_try_futures).await;
println!("try_join_all empty: {:?}", empty_result); // Ok([])
}Both handle empty collections gracefully, returning empty results.
Concurrent Timeout Handling
use futures::future::{join_all, try_join_all};
use tokio::time::{timeout, Duration};
async fn fetch_with_timeout(url: &str) -> Result<String, String> {
timeout(Duration::from_millis(100), async {
// Simulate slow fetch
if url.contains("slow") {
tokio::time::sleep(Duration::from_secs(10)).await;
}
Ok(format!("Data from {}", url))
}).await.map_err(|_| format!("Timeout for {}", url))?
}
#[tokio::main]
async fn main() {
let urls = vec!["fast1", "slow", "fast2"];
// With join_all: all futures complete (or timeout individually)
let futures: Vec<_> = urls.iter().map(|url| fetch_with_timeout(url)).collect();
let results: Vec<Result<String, String>> = join_all(futures).await;
println!("join_all results:");
for (i, result) in results.iter().enumerate() {
println!(" {}: {:?}", urls[i], result);
}
// With try_join_all: first timeout fails the whole operation
let futures: Vec<_> = urls.iter().map(|url| fetch_with_timeout(url)).collect();
let result: Result<Vec<String>, String> = try_join_all(futures).await;
println!("try_join_all result: {:?}", result);
}join_all collects timeout errors; try_join_all propagates the first timeout.
Collecting Successful Results After Failure
use futures::future::join_all;
#[tokio::main]
async fn main() {
let tasks = vec![
async { Ok::<i32, &str>(1) },
async { Err::<i32, &str>("task 2 failed") },
async { Ok::<i32, &str>(3) },
async { Err::<i32, &str>("task 4 failed") },
async { Ok::<i32, &str>(5) },
];
// join_all gives all results
let results: Vec<Result<i32, &str>> = join_all(tasks).await;
// Filter successful ones
let successes: Vec<i32> = results.iter()
.filter_map(|r| r.as_ref().ok().copied())
.collect();
let failures: Vec<&str> = results.iter()
.filter_map(|r| r.as_ref().err().copied())
.collect();
println!("Successful: {:?}", successes); // [1, 3, 5]
println!("Failed: {:?}", failures); // ["task 2 failed", "task 4 failed"]
}join_all enables extracting partial success from mixed results.
Error Type Requirements
use futures::future::try_join_all;
use std::fmt;
#[derive(Debug)]
enum MyError {
Network(String),
Timeout(String),
}
impl fmt::Display for MyError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
MyError::Network(s) => write!(f, "Network error: {}", s),
MyError::Timeout(s) => write!(f, "Timeout: {}", s),
}
}
}
impl std::error::Error for MyError {}
#[tokio::main]
async fn main() {
// try_join_all requires all errors to be compatible
let tasks = vec![
async { Ok::<i32, MyError>(1) },
async { Err::<i32, MyError>(MyError::Network("connection failed".into())) },
async { Ok::<i32, MyError>(3) },
];
// Works because all tasks return same error type
let result: Result<Vec<i32>, MyError> = try_join_all(tasks).await;
println!("Result: {:?}", result);
// Different error types need conversion
let heterogeneous = vec![
async { Ok::<i32, String>(1).map_err(|e| MyError::Network(e)) },
async { Ok::<i32, &str>(2).map_err(|e| MyError::Network(e.to_string())) },
];
let _ = try_join_all(heterogeneous).await;
}try_join_all requires compatible error types across all futures.
Performance Implications
use futures::future::{join_all, try_join_all};
use std::time::Instant;
use tokio::time::{sleep, Duration};
#[tokio::main]
async fn main() {
// Scenario: 100 tasks, one failing early
// join_all: waits for all 100 tasks
let tasks: Vec<_> = (0..100).map(|i| async move {
sleep(Duration::from_millis(i)).await;
if i == 0 {
Err::<i32, &str>("fail")
} else {
Ok(i)
}
}).collect();
let start = Instant::now();
let _ = join_all(tasks).await;
println!("join_all (first fails): {:?}", start.elapsed());
// Waits for all 100 tasks despite early failure
// try_join_all: returns immediately on first failure
let tasks: Vec<_> = (0..100).map(|i| async move {
sleep(Duration::from_millis(i)).await;
if i == 0 {
Err::<i32, &str>("fail")
} else {
Ok(i)
}
}).collect();
let start = Instant::now();
let _ = try_join_all(tasks).await;
println!("try_join_all (first fails): {:?}", start.elapsed());
// Returns as soon as first failure is detected
}try_join_all can be faster when failures occur early.
Converting Between Patterns
use futures::future::{join_all, try_join_all};
#[tokio::main]
async fn main() {
// Converting try_join_all semantics to join_all
let tasks = vec![
async { Ok::<i32, &str>(1) },
async { Err::<i32, &str>("fail") },
async { Ok::<i32, &str>(3) },
];
// try_join_all short-circuits
let result = try_join_all(tasks).await;
println!("try_join_all: {:?}", result); // Err("fail")
// Simulate with join_all + check
let tasks = vec![
async { Ok::<i32, &str>(1) },
async { Err::<i32, &str>("fail") },
async { Ok::<i32, &str>(3) },
];
let results: Vec<Result<i32, &str>> = join_all(tasks).await;
let first_error = results.iter().find_map(|r| r.as_ref().err());
match first_error {
Some(e) => println!("join_all manual: Err({:?})", e),
None => {
let success: Vec<_> = results.into_iter().map(|r| r.unwrap()).collect();
println!("join_all manual: Ok({:?})", success);
}
}
}You can simulate try_join_all semantics with join_all plus error checking.
Synthesis
Core distinction:
join_allreturnsVec<T>after all futures completetry_join_allreturnsResult<Vec<T>, E>and short-circuits on first error
Return types:
join_all:Vec<T>where T is the future's output (can beResult)try_join_all:Result<Vec<T>, E>where futures must returnResult<T, E>
Error handling:
join_all: All futures complete; collect successes and failures separatelytry_join_all: First error stops execution; other futures may be cancelled
When to use join_all:
- Partial success is meaningful (e.g., checking multiple services)
- Need all results regardless of failures
- Collecting and reporting multiple errors
- Independent operations where one failure doesn't affect others
When to use try_join_all:
- All futures must succeed for overall success
- Want early termination on first failure
- Transaction-like semantics where partial failure rejects all
- Want standard
Resultpropagation with?operator
Key insight: Both execute futures concurrently, but join_all is "collect everything" while try_join_all is "all or nothing". Choose based on whether partial success has value in your application logic. For health checks, data collection from multiple sources, or best-effort operations, use join_all. For atomic operations, initialization sequences, or transactions, use try_join_all.
