What is the difference between futures::future::join_all and futures::future::try_join_all for error handling?
futures::future::join_all runs all futures to completion regardless of failures, collecting results into a Vec<Result<T, E>> where individual failures are represented as Err values in the vector. futures::future::try_join_all short-circuits on the first error, immediately returning Err without waiting for other futures to complete and discarding their results. The key distinction is that join_all guarantees all futures finish and gives you access to every result, while try_join_all provides early termination for fail-fast semantics where you only care whether everything succeeded. Use join_all when you need to process all results including failures, and try_join_all when any failure should abort the entire operation.
Basic join_all Usage
use futures::future::join_all;
async fn fetch_urls() -> Vec<Result<String, reqwest::Error>> {
let urls = vec![
"https://example.com/1",
"https://example.com/2",
"https://example.com/3",
];
let futures: Vec<_> = urls
.into_iter()
.map(|url| reqwest::get(url))
.collect();
// join_all runs all futures to completion
let results = join_all(futures).await;
// Returns Vec<Result<String, Error>>
// All three requests complete, success or failure
results
}join_all returns all results in a vector.
Basic try_join_all Usage
use futures::future::try_join_all;
async fn fetch_all_or_fail() -> Result<Vec<String>, reqwest::Error> {
let urls = vec![
"https://example.com/1",
"https://example.com/2",
"https://example.com/3",
];
let futures: Vec<_> = urls
.into_iter()
.map(|url| reqwest::get(url))
.collect();
// try_join_all returns Err on first failure
let results: Result<Vec<_>, _> = try_join_all(futures).await;
// If any request fails, returns Err immediately
// Only returns Ok if ALL succeed
results
}try_join_all returns Err on first failure.
join_all Collects All Results
use futures::future::join_all;
async fn example() {
let futures = vec![
async { Ok::<i32, &str>(1) },
async { Err::<i32, &str>("failed") },
async { Ok::<i32, &str>(3) },
];
let results = join_all(futures).await;
// All futures complete
assert_eq!(results.len(), 3);
assert_eq!(results[0], Ok(1));
assert_eq!(results[1], Err("failed"));
assert_eq!(results[2], Ok(3));
}join_all always collects all results, including failures.
try_join_all Short-Circuits on Error
use futures::future::try_join_all;
async fn example() {
let futures = vec![
async { Ok::<i32, &str>(1) },
async { Err::<i32, &str>("failed") },
async { Ok::<i32, &str>(3) },
];
let result: Result<Vec<i32>, &str> = try_join_all(futures).await;
// Returns Err on first failure
assert_eq!(result, Err("failed"));
// Note: The third future may or may not have completed
// try_join_all doesn't wait for it once error occurs
}try_join_all stops at first error, other futures may be cancelled.
Return Types Compared
use futures::future::{join_all, try_join_all};
async fn compare_types() {
let futures = vec![
async { Ok::<i32, &str>(1) },
async { Ok::<i32, &str>(2) },
];
// join_all returns Vec<Result<T, E>>
let join_results: Vec<Result<i32, &str>> = join_all(futures.clone()).await;
// try_join_all returns Result<Vec<T>, E>
let try_join_result: Result<Vec<i32>, &str> = try_join_all(futures).await;
// When all succeed:
// join_all: vec![Ok(1), Ok(2)]
// try_join_all: Ok(vec![1, 2])
}join_all wraps each result; try_join_all wraps the whole collection.
Processing All Results with join_all
use futures::future::join_all;
async fn fetch_all_data(urls: Vec<&str>) -> Vec<String> {
let futures: Vec<_> = urls
.into_iter()
.map(|url| async move {
reqwest::get(url).await?.text().await
})
.collect();
let results = join_all(futures).await;
// Process successes and failures separately
let mut successes = Vec::new();
let mut failures = Vec::new();
for result in results {
match result {
Ok(data) => successes.push(data),
Err(e) => failures.push(e),
}
}
println!("Succeeded: {}", successes.len());
println!("Failed: {}", failures.len());
successes
}Use join_all when you need to know which operations failed.
Fail-Fast with try_join_all
use futures::future::try_join_all;
async fn execute_transaction(operations: Vec<Operation>) -> Result<(), Error> {
// All operations must succeed for transaction to commit
let futures: Vec<_> = operations
.into_iter()
.map(|op| execute_operation(op))
.collect();
// If any operation fails, abort immediately
try_join_all(futures).await?;
// All succeeded
Ok(())
}Use try_join_all when any failure should abort the whole operation.
Error Propagation Differences
use futures::future::{join_all, try_join_all};
async fn error_handling() {
let futures = vec![
async { Err::<i32, &str>("error1") },
async { Err::<i32, &str>("error2") },
async { Err::<i32, &str>("error3") },
];
// join_all: all errors are collected
let results = join_all(futures.clone()).await;
// results = vec![Err("error1"), Err("error2"), Err("error3")]
// You can see all errors
// try_join_all: only first error is returned
let result = try_join_all(futures).await;
// result = Err("error1") (or "error2" or "error3" depending on completion order)
// Only one error visible
}join_all preserves all errors; try_join_all returns only the first.
Parallel Fetch with Partial Success
use futures::future::join_all;
async fn fetch_with_fallback(urls: Vec<&str>) -> Option<String> {
let futures: Vec<_> = urls
.into_iter()
.map(|url| reqwest::get(url))
.collect();
let results = join_all(futures).await;
// Return first successful result
for result in results {
if let Ok(response) = result {
if let Ok(text) = response.text().await {
return Some(text);
}
}
}
None
}join_all lets you pick successful results from a batch.
Transaction Pattern with try_join_all
use futures::future::try_join_all;
async fn commit_all_or_none(items: Vec<Item>) -> Result<(), Error> {
// Prepare all items first (validation)
let prepared: Vec<_> = items
.into_iter()
.map(|item| prepare_item(item))
.collect();
// Try to commit all - if any fails, none should be considered committed
let futures: Vec<_> = prepared
.into_iter()
.map(|item| commit_item(item))
.collect();
try_join_all(futures).await?;
// All committed successfully
Ok(())
}try_join_all for atomic-style "all or nothing" semantics.
Handling Mixed Results
use futures::future::join_all;
#[derive(Debug)]
struct FetchResult {
url: String,
data: Option<String>,
error: Option<String>,
}
async fn fetch_all_with_context(urls: Vec<String>) -> Vec<FetchResult> {
let futures: Vec<_> = urls
.iter()
.map(|url| async {
match reqwest::get(url).await {
Ok(response) => match response.text().await {
Ok(text) => (url.clone(), Some(text), None),
Err(e) => (url.clone(), None, Some(e.to_string())),
},
Err(e) => (url.clone(), None, Some(e.to_string())),
}
})
.collect();
let results = join_all(futures).await;
results
.into_iter()
.map(|(url, data, error)| FetchResult { url, data, error })
.collect()
}join_all preserves context about which URL failed.
Early Exit with try_join_all
use futures::future::try_join_all;
async fn validate_all(inputs: Vec<Input>) -> Result<Vec<Validated>, ValidationError> {
let futures: Vec<_> = inputs
.into_iter()
.map(|input| validate(input))
.collect();
// Stop on first validation error
// Don't waste time validating remaining inputs
try_join_all(futures).await
}try_join_all for fail-fast validation.
Concurrent Tasks with Cleanup
use futures::future::join_all;
async fn run_all_tasks(tasks: Vec<Task>) -> Vec<TaskResult> {
let futures: Vec<_> = tasks
.into_iter()
.map(|task| run_task(task))
.collect();
// Run all tasks, collect all results
let results = join_all(futures).await;
// Log all outcomes
for (task, result) in tasks.iter().zip(results.iter()) {
match result {
Ok(output) => println!("Task {} completed: {}", task.id, output),
Err(e) => println!("Task {} failed: {}", task.id, e),
}
}
results
}join_all ensures all tasks complete and results are logged.
Timeout with join_all
use futures::future::join_all;
use tokio::time::{timeout, Duration};
async fn fetch_with_timeout(urls: Vec<&str>) -> Vec<Result<String, Error>> {
let futures: Vec<_> = urls
.into_iter()
.map(|url| async {
timeout(Duration::from_secs(5), reqwest::get(url))
.await
.map_err(|_| Error::Timeout)?
.map_err(Error::Request)?
.text()
.await
.map_err(Error::Request)
})
.collect();
join_all(futures).await
}Apply timeouts individually with join_all to get per-result timeout info.
Cancellation Behavior
use futures::future::{join_all, try_join_all};
async fn cancellation_example() {
// join_all: all futures run to completion
// Even if one fails, others continue
let futures = vec![
async { Err::<i32, &str>("fail") },
async {
tokio::time::sleep(Duration::from_secs(10)).await;
Ok::<i32, &str>(1)
},
];
let results = join_all(futures).await;
// Both futures complete, 10 seconds total
// try_join_all: cancels remaining futures on error
let futures = vec![
async { Err::<i32, &str>("fail") },
async {
tokio::time::sleep(Duration::from_secs(10)).await;
Ok::<i32, &str>(1)
},
];
let result = try_join_all(futures).await;
// Returns Err("fail") immediately
// Second future may be cancelled
}try_join_all may cancel remaining futures on error.
Combining with Other Futures
use futures::future::{join_all, try_join_all, join};
async fn combined_example() {
// join_all for independent operations where we want all results
let fetch_results = join_all(vec![
fetch_url("url1"),
fetch_url("url2"),
]);
// try_join_all for dependent operations where any failure aborts
let db_results = try_join_all(vec![
insert_record(1),
insert_record(2),
]);
// Combine different types
let (fetches, inserts) = join(fetch_results, db_results).await;
// fetches: Vec<Result<String, Error>>
// inserts: Result<Vec<()>, Error>
}Mix join_all and try_join_all based on requirements.
Real-World: Batch Processing
use futures::future::join_all;
struct BatchResult {
successes: Vec<ProcessedItem>,
failures: Vec<(Item, Error)>,
}
async fn process_batch(items: Vec<Item>) -> BatchResult {
let futures: Vec<_> = items
.into_iter()
.map(|item| async { process_item(item).await.map(|processed| (item, processed)) })
.collect();
let results = join_all(futures).await;
let mut batch_result = BatchResult {
successes: Vec::new(),
failures: Vec::new(),
};
for (item, result) in results {
match result {
Ok(processed) => batch_result.successes.push(processed),
Err(e) => batch_result.failures.push((item, e)),
}
}
batch_result
}Batch processing where you want to know exactly what failed.
Real-World: Dependent Operations
use futures::future::try_join_all;
async fn setup_infrastructure(services: Vec<ServiceConfig>) -> Result<(), Error> {
// All services must start successfully
let futures: Vec<_> = services
.into_iter()
.map(|config| start_service(config))
.collect();
// If any service fails to start, abort setup
try_join_all(futures).await?;
println!("All services started successfully");
Ok(())
}Infrastructure setup where all services must start.
Error Aggregation Pattern
use futures::future::join_all;
#[derive(Debug)]
struct AggregateError {
errors: Vec<(usize, String)>,
}
async fn fetch_all_or_aggregate_errors(urls: Vec<&str>) -> Result<Vec<String>, AggregateError> {
let futures: Vec<_> = urls
.into_iter()
.enumerate()
.map(|(i, url)| async move {
let result = reqwest::get(url).await?.text().await;
result.map(|text| (i, text)).map_err(|e| (i, e.to_string()))
})
.collect();
let results = join_all(futures).await;
let mut successes = Vec::new();
let mut errors = Vec::new();
for result in results {
match result {
Ok((i, text)) => successes.push((i, text)),
Err((i, e)) => errors.push((i, e)),
}
}
if errors.is_empty() {
Ok(successes.into_iter().map(|(_, text)| text).collect())
} else {
Err(AggregateError { errors })
}
}Collect all errors with join_all for comprehensive error reporting.
Performance Considerations
use futures::future::{join_all, try_join_all};
// join_all: All futures run to completion
// - Memory: O(n) for results
// - Time: max(future_times), parallel execution
// - Use when you need all results
// try_join_all: May cancel futures on error
// - Memory: O(n) initially, releases on completion
// - Time: min(failure_time) for early exit, max(future_times) for success
// - Use when failures should abort
async fn performance_example() {
// Scenario: 10 requests, one at 1s fails
// join_all: waits for all 10 (takes max time)
// try_join_all: returns at 1s when first fails
}try_join_all can be faster when failures occur early.
When to Use Each
// Use join_all when:
// - You need all results, including failures
// - Partial success is acceptable
// - You want to aggregate errors
// - Each result needs individual handling
// - Cancellation isn't desired
// Use try_join_all when:
// - Any failure should abort the operation
// - You want fail-fast semantics
// - Partial results are useless
// - You want early termination on error
// - All-or-nothing semantics needed
async fn choose_wisely() {
// Example: fetching multiple URLs
// join_all: get all responses, show what worked
// try_join_all: abort if any URL fails
// Example: writing to multiple databases
// join_all: write to all, report failures
// try_join_all: transaction semantics - all or none
// Example: validation
// join_all: collect all validation errors
// try_join_all: stop at first validation error
}Choose based on whether you need all results or fast failure.
Comparison Table
| Aspect | join_all |
try_join_all |
|---|---|---|
| Return type | Vec<Result<T, E>> |
Result<Vec<T>, E> |
| On error | Continue, collect all | Short-circuit, return first error |
| Cancellation | None (all complete) | May cancel remaining |
| Error visibility | All errors preserved | Only first error |
| Use case | Partial success OK | Fail-fast required |
| Success case | vec![Ok(v1), Ok(v2)] |
Ok(vec![v1, v2]) |
Synthesis
join_all and try_join_all represent two different philosophies for handling concurrent operations:
join_all is optimistic and thorough. It runs every future to completion and gives you every result. Failures are data—individual Err values in a vector of Results. Use it when:
- You need to know which operations failed
- Partial success is acceptable or useful
- You want to aggregate multiple errors
- Each result requires individual handling
- Operations are independent and their results are useful independently
try_join_all is fail-fast. It returns immediately on the first error, discarding other results and potentially canceling remaining futures. Use it when:
- All operations must succeed for the result to be useful
- Early termination on error is desirable
- You want simple error propagation (
?operator friendly) - The operation has transaction semantics
- Continuing after failure wastes resources
Key insight: The choice isn't just about error handling syntax—it's about what you're trying to accomplish. If you're building a dashboard that shows multiple API results, use join_all because you want to show what's available and indicate what failed. If you're writing to multiple databases as a transaction, use try_join_all because a partial write is worse than no write at all.
