How does futures::future::join_all handle errors compared to try_join_all for concurrent futures?
join_all runs all futures to completion regardless of individual failures, collecting all results in a Vec<Result<T, E>>, while try_join_all short-circuits on the first error and returns immediately with Err, not waiting for remaining futures to complete. The key distinction is error propagation behavior: join_all lets you see all successes and failures together, whereas try_join_all fails fast, returning the first error and potentially canceling remaining work.
Basic join_all Behavior
use futures::future::join_all;
async fn join_all_example() {
// join_all runs all futures and collects all results
// Errors are included in the result, not propagated
let futures = vec![
async { Ok::<_, &'static str>(1) },
async { Err::<i32, _>("error 1") },
async { Ok(3) },
async { Err("error 2") },
];
// join_all returns Vec<Result<T, E>>
let results: Vec<Result<i32, &str>> = join_all(futures).await;
// ALL futures ran to completion
// Results include both successes and failures
assert_eq!(results.len(), 4);
assert_eq!(results[0], Ok(1));
assert_eq!(results[1], Err("error 1"));
assert_eq!(results[2], Ok(3));
assert_eq!(results[3], Err("error 2"));
// No short-circuit: all futures completed
// You decide how to handle errors
}join_all collects all results, letting you examine all successes and failures.
Basic try_join_all Behavior
use futures::future::try_join_all;
async fn try_join_all_example() {
// try_join_all short-circuits on first error
let futures = vec![
async { Ok::<_, &'static str>(1) },
async { Err::<i32, _>("error 1") },
async { Ok(3) },
async { Err("error 2") },
];
// try_join_all returns Result<Vec<T>, E>
let result: Result<Vec<i32>, &str> = try_join_all(futures).await;
// Short-circuits on first error
assert_eq!(result, Err("error 1"));
// Remaining futures may or may not have started
// No guarantee about completion of non-error futures
// Returns only the first error encountered
}try_join_all fails fast, returning the first error without waiting for others.
Result Type Differences
use futures::future::{join_all, try_join_all};
async fn result_types() {
// join_all: Vec<Result<T, E>>
// - Always returns Vec (all futures completed)
// - Each element is the individual future's result
// - You handle errors per-element
let futures = vec![
async { Ok::<_, &'static str>(1) },
async { Err::<i32, _>("error") },
];
let results: Vec<Result<i32, &str>> = join_all(futures).await;
// Process each result individually
for result in results {
match result {
Ok(value) => println!("Success: {}", value),
Err(e) => println!("Error: {}", e),
}
}
// try_join_all: Result<Vec<T>, E>
// - Returns Err on first failure
// - On success, Vec contains all T values (not Result<T, E>)
let futures = vec![
async { Ok::<_, &'static str>(1) },
async { Ok(2) },
];
let result: Result<Vec<i32>, &str> = try_join_all(futures).await;
// On success, Vec contains unwrapped values
match result {
Ok(values) => {
// values: Vec<i32> (not Vec<Result<i32, &str>>)
for value in values {
println!("Value: {}", value);
}
}
Err(e) => println!("Failed: {}", e),
}
}join_all returns Vec<Result<T, E>>; try_join_all returns Result<Vec<T>, E>.
Error Handling Patterns
use futures::future::{join_all, try_join_all};
async fn error_handling_patterns() {
// Pattern 1: join_all for partial success handling
async fn fetch_item(id: u32) -> Result<String, &'static str> {
if id % 2 == 0 {
Ok(format!("Item {}", id))
} else {
Err("Failed to fetch")
}
}
let futures: Vec<_> = (0..5).map(fetch_item).collect();
let results = join_all(futures).await;
// Process successes, log failures
let successes: Vec<_> = results.iter()
.filter_map(|r| r.as_ref().ok())
.collect();
let failures: Vec<_> = results.iter()
.filter_map(|r| r.as_ref().err())
.collect();
println!("Succeeded: {:?}", successes);
println!("Failed: {:?}", failures);
// Pattern 2: try_join_all for all-or-nothing semantics
async fn validate_field(field: &str) -> Result<(), &'static str> {
if field.len() > 3 {
Ok(())
} else {
Err("Field too short")
}
}
let fields = vec!["username", "email", "age"];
let futures: Vec<_> = fields.iter().map(|f| validate_field(f)).collect();
match try_join_all(futures).await {
Ok(_) => println!("All validations passed"),
Err(e) => println!("Validation failed: {}", e),
}
// Use try_join_all when any failure invalidates the whole operation
}Use join_all when you need all results including failures; use try_join_all when any failure should abort.
Cancellation and Short-Circuit Behavior
use futures::future::{join_all, try_join_all};
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::Arc;
async fn cancellation_behavior() {
// join_all: No cancellation, all futures run
let counter = Arc::new(AtomicU32::new(0));
let futures: Vec<_> = (0..5).map(|i| {
let counter = counter.clone();
async move {
counter.fetch_add(1, Ordering::SeqCst);
if i == 2 {
Err::<i32, _>("error")
} else {
Ok(i)
}
}
}).collect();
let results = join_all(futures).await;
// All futures ran
assert_eq!(counter.load(Ordering::SeqCst), 5);
// try_join_all: May not run all futures
let counter = Arc::new(AtomicU32::new(0));
// Note: Actual cancellation behavior depends on
// how futures are structured. try_join_all returns
// as soon as error is found, but already-running
// futures continue unless explicitly dropped.
let futures: Vec<_> = (0..5).map(|i| {
let counter = counter.clone();
async move {
counter.fetch_add(1, Ordering::SeqCst);
if i == 2 {
Err::<i32, _>("error")
} else {
Ok(i)
}
}
}).collect();
let result = try_join_all(futures).await;
// result is Err("error")
// All futures may have started, but we don't wait for all
// The exact number that ran depends on scheduling
}join_all guarantees all futures complete; try_join_all may return early.
Practical Use Cases
use futures::future::{join_all, try_join_all};
async fn practical_use_cases() {
// Use join_all for:
// 1. Parallel data fetching where partial results are useful
// 2. Aggregating results from multiple sources
// 3. Bulk operations with individual error reporting
async fn fetch_user(id: u32) -> Result<User, ApiError> {
// Fetch user from API
Ok(User { id, name: "test".to_string() })
}
struct User { id: u32, name: String }
struct ApiError;
// Fetch multiple users, handle each result
let user_ids = vec![1, 2, 3, 4, 5];
let futures: Vec<_> = user_ids.iter().map(|id| fetch_user(*id)).collect();
let results = join_all(futures).await;
// Process successful fetches, log failures
let users: Vec<_> = results.into_iter()
.filter_map(|r| r.ok())
.collect();
// Use try_join_all for:
// 1. Validation steps where all must succeed
// 2. Transaction-like operations
// 3. Initialization where any failure aborts
async fn initialize_service(name: &str) -> Result<(), String> {
if name.is_empty() {
Err("Service name cannot be empty".to_string())
} else {
Ok(())
}
}
let services = vec!["auth", "database", "cache"];
let futures: Vec<_> = services.iter()
.map(|s| initialize_service(s))
.collect();
match try_join_all(futures).await {
Ok(_) => println!("All services initialized"),
Err(e) => println!("Initialization failed: {}", e),
}
}Choose join_all for partial success handling; try_join_all for all-or-nothing semantics.
Working with Empty Collections
use futures::future::{join_all, try_join_all};
async fn empty_collections() {
// join_all with empty collection
let empty: Vec<std::pin::Pin<Box<dyn std::future::Future<Output = Result<i32, &str>>>>> = vec![];
let results = join_all(empty).await;
// Returns empty Vec
assert!(results.is_empty());
// try_join_all with empty collection
let empty: Vec<std::pin::Pin<Box<dyn std::future::Future<Output = Result<i32, &str>>>>> = vec![];
let result = try_join_all(empty).await;
// Returns Ok(Vec::new())
assert!(result.is_ok());
assert!(result.unwrap().is_empty());
// Both handle empty collections gracefully
// join_all: returns empty Vec
// try_join_all: returns Ok(empty Vec)
}Both functions handle empty collections gracefully, returning empty vectors.
Combining with Other Futures
use futures::future::{join_all, try_join_all, join3, try_join3};
async fn combining_futures() {
// join_all can be combined with other join functions
let fut1 = async { Ok::<_, ()>(1) };
let fut2 = async { Ok(2) };
let fut3 = async { Ok(3) };
// Using try_join3 for three futures
let result = try_join3(fut1, fut2, fut3).await;
assert_eq!(result, Ok((1, 2, 3)));
// Using try_join_all for dynamic number of futures
let futures = vec![
async { Ok::<_, ()>(1) },
async { Ok(2) },
async { Ok(3) },
];
let result = try_join_all(futures).await;
assert_eq!(result, Ok(vec![1, 2, 3]));
// Key difference: try_join_all works with Vec
// try_join3 works with 3 specific futures
// Result types differ:
// - try_join3: Result<(T1, T2, T3), E>
// - try_join_all: Result<Vec<T>, E>
// Similar pattern with join_all vs join3
let result = join_all(vec![
async { Ok::<_, ()>(1) },
async { Ok(2) },
]).await;
// Result: Vec<Result<i32, ()>>
let result = join3(
async { Ok::<_, ()>(1) },
async { Ok(2) },
async { Ok(3) },
).await;
// Result: (Result<i32, ()>, Result<i32, ()>, Result<i32, ()>)
}Use join_all/try_join_all for dynamic collections; use join3/try_join3 for fixed numbers.
Error Propagation with ?
use futures::future::{join_all, try_join_all};
async fn error_propagation() {
// try_join_all works naturally with ?
async fn process_all() -> Result<Vec<i32>, &'static str> {
let futures = vec![
async { Ok::<_, &str>(1) },
async { Ok(2) },
async { Ok(3) },
];
// try_join_all returns Result<Vec<T>, E>
// Works directly with ?
let values = try_join_all(futures).await?;
// values is Vec<i32> here
Ok(values.into_iter().map(|v| v * 2).collect())
}
let result = process_all().await;
assert_eq!(result, Ok(vec![2, 4, 6]));
// With error:
async fn process_with_error() -> Result<Vec<i32>, &'static str> {
let futures = vec![
async { Ok::<_, &str>(1) },
async { Err("failed") },
async { Ok(3) },
];
// ? propagates the error
let values = try_join_all(futures).await?;
// Never reaches here
Ok(values)
}
let result = process_with_error().await;
assert_eq!(result, Err("failed"));
// join_all doesn't propagate errors with ?
// You need to handle errors manually
async fn process_join_all() -> Result<Vec<i32>, &'static str> {
let futures = vec![
async { Ok::<_, &str>(1) },
async { Err("failed") },
];
let results = join_all(futures).await;
// Manually check for errors
let values: Result<Vec<_>, _> = results.into_iter().collect();
values
}
}try_join_all works naturally with ? for error propagation; join_all requires manual error handling.
Performance Considerations
use futures::future::{join_all, try_join_all};
async fn performance() {
// join_all: Waits for ALL futures
// - Total time = max(future times)
// - All futures run to completion
// - Memory: All results stored
// try_join_all: May return early
// - On error: time = time to first error
// - On success: time = max(future times)
// - May cancel remaining work
// Example: Network requests with timeouts
async fn fetch_with_timeout(id: u32) -> Result<String, &'static str> {
// Simulate varying response times
if id == 2 {
Err("timeout") // Fast failure
} else {
Ok(format!("Item {}", id))
}
}
let futures: Vec<_> = (0..100).map(fetch_with_timeout).collect();
// With try_join_all:
// - Returns quickly when id==2 fails
// - Don't wait for all 100 requests
let result = try_join_all(futures).await;
assert!(result.is_err());
// With join_all:
// - Waits for all 100 futures
// - Then you can see which failed
// For latency-sensitive code with possible fast failures:
// try_join_all can return sooner
}try_join_all can return faster on errors; join_all always waits for all futures.
Handling Individual Errors
use futures::future::join_all;
async fn handling_individual_errors() {
// When you need detailed error information
async fn process_item(id: u32) -> Result<ProcessedItem, ProcessingError> {
if id % 3 == 0 {
Err(ProcessingError::InvalidId)
} else if id % 5 == 0 {
Err(ProcessingError::Timeout)
} else {
Ok(ProcessedItem { id })
}
}
#[derive(Debug)]
struct ProcessedItem { id: u32 }
#[derive(Debug)]
enum ProcessingError {
InvalidId,
Timeout,
}
let futures: Vec<_> = (0..10).map(process_item).collect();
let results = join_all(futures).await;
// Categorize errors
let (successes, failures): (Vec<_>, Vec<_>) = results.into_iter()
.partition(Result::is_ok);
let successes: Vec<_> = successes.into_iter().map(Result::unwrap).collect();
let failures: Vec<_> = failures.into_iter().map(Result::unwrap_err).collect();
println!("Successful: {:?}", successes);
println!("Failed: {:?}", failures);
// With try_join_all, you'd lose this detail
// Only see the first error
}join_all preserves all error information; try_join_all only returns the first error.
Synthesis
Core difference:
// join_all: Collect all results, including errors
let results: Vec<Result<T, E>> = join_all(futures).await;
// Waits for ALL futures
// Returns Vec of individual results
// try_join_all: Short-circuit on first error
let result: Result<Vec<T>, E> = try_join_all(futures).await;
// Returns immediately on first error
// On success: Vec<T> (unwrapped values)Error handling:
| Function | Return Type | Error Behavior | Use Case |
|---|---|---|---|
join_all |
Vec<Result<T, E>> |
Collects all, includes errors | Partial success |
try_join_all |
Result<Vec<T>, E> |
Short-circuits on first error | All-or-nothing |
When to use each:
// Use join_all when:
// - You need all results (success and failure)
// - Partial success is meaningful
// - You want to log/analyze all failures
// - Error handling is per-item
let results = join_all(futures).await;
let successes: Vec<_> = results.iter().filter_map(|r| r.as_ref().ok()).collect();
let failures: Vec<_> = results.iter().filter_map(|r| r.as_ref().err()).collect();
// Use try_join_all when:
// - Any failure invalidates the whole operation
// - You want early termination on error
// - You're using ? for error propagation
// - All-or-nothing semantics required
let values = try_join_all(futures).await?; // Propagates first errorKey insight: join_all and try_join_all represent two fundamentally different approaches to concurrent error handling—join_all is "collect everything and sort it out later," useful when you need visibility into all outcomes, while try_join_all is "fail fast and propagate," useful when the entire operation is invalidated by any single failure. With join_all, you get Vec<Result<T, E>> and can process successes and failures independently, logging errors while proceeding with valid results. With try_join_all, you get Result<Vec<T>, E> where success means all values are unwrapped, and failure means the first error encountered. Choose join_all for partial success handling, retry logic, or detailed error reporting; choose try_join_all for atomic operations, validation, or when using ? for error propagation. The performance difference matters only when errors occur early—try_join_all can return immediately on first error while join_all waits for all futures regardless of individual failures.
