What is the difference between futures::future::join_all and try_join_all for error handling semantics?
join_all runs all futures to completion regardless of individual failures and returns Vec<T> containing all results, while try_join_all short-circuits on the first error and returns Result<Vec<T>, E> where the error from the first failed future propagates immediately. The fundamental distinction is failure semantics: join_all collects everything including errors (they become values), try_join_all treats errors as failures that abort the entire operation.
The Shared Foundation: Running Multiple Futures
use futures::future::{join_all, try_join_all};
async fn basics() {
// Both functions take a collection of futures and run them concurrently
// Both return a single future that resolves when all input futures complete
let futures = vec![
async { 1 },
async { 2 },
async { 3 },
];
// join_all returns all results
let results: Vec<i32> = join_all(futures).await;
assert_eq!(results, vec![1, 2, 3]);
// try_join_all requires futures that return Result
let futures = vec![
async { Ok::<_, ()>(1) },
async { Ok(2) },
async { Ok(3) },
];
let results: Result<Vec<i32>, ()> = try_join_all(futures).await;
assert_eq!(results, Ok(vec![1, 2, 3]));
}Both functions execute futures concurrently and wait for all to complete.
The Return Type Difference
use futures::future::{join_all, try_join_all};
async fn return_types() {
// join_all return type: Vec<T>
// Where each future has Output = T
// Result is always Vec, never Err
let futures: Vec<std::pin::Pin<Box<dyn futures::Future<Output = i32>>>> = vec![
Box::pin(async { 1 }),
Box::pin(async { 2 }),
];
let result: Vec<i32> = join_all(futures).await;
// result is always Vec<i32>, cannot be an error
// try_join_all return type: Result<Vec<T>, E>
// Where each future has Output = Result<T, E>
// Result can be Ok(Vec<T>) or Err(E)
let futures: Vec<std::pin::Pin<Box<dyn futures::Future<Output = Result<i32, String>>>>> = vec![
Box::pin(async { Ok(1) }),
Box::pin(async { Ok(2) }),
];
let result: Result<Vec<i32>, String> = try_join_all(futures).await;
// result can be Ok or Err
}The output types reflect the different error handling strategies.
join_all: Collect Everything
use futures::future::join_all;
async fn join_all_behavior() {
// join_all runs ALL futures to completion
// Results are collected into Vec, including "failures"
// With infallible futures (no Result)
let futures = vec![
async { 1 },
async { 2 },
async { 3 },
];
let results = join_all(futures).await;
assert_eq!(results, vec![1, 2, 3]);
// With fallible futures (with Result)
// Results ARE collected, errors are just values
let futures = vec![
async { Ok::<i32, &str>(1) },
async { Err("failed") },
async { Ok(3) },
];
// join_all returns Vec<Result<i32, &str>>
// The Err is just a value in the vec, not a short-circuit
let results: Vec<Result<i32, &str>> = join_all(futures).await;
assert_eq!(results, vec![Ok(1), Err("failed"), Ok(3)]);
// ALL futures ran, including the one after Err
// The Err did NOT stop execution
}join_all treats errors as valuesâevery future runs, every result is collected.
try_join_all: Short-Circuit on Error
use futures::future::try_join_all;
async fn try_join_all_behavior() {
// try_join_all short-circuits on the FIRST error
// Returns immediately with the error
let futures = vec![
async { Ok::<i32, &str>(1) },
async { Err("failed") },
async { Ok(3) }, // May not run
];
let result: Result<Vec<i32>, &str> = try_join_all(futures).await;
assert_eq!(result, Err("failed"));
// The third future may not complete
// The second future's error propagated immediately
// Result is Err, not Ok with errors inside
}try_join_all treats errors as failuresâfirst error stops everything and propagates.
Critical Difference: Error Propagation
use futures::future::{join_all, try_join_all};
async fn error_propagation() {
// join_all: Errors are collected, not propagated
let futures = vec![
async { Ok::<i32, &str>(1) },
async { Err("error 1") },
async { Err("error 2") },
async { Ok(4) },
];
let results: Vec<Result<i32, &str>> = join_all(futures).await;
// ALL results are present
assert_eq!(results.len(), 4);
assert_eq!(results, vec![Ok(1), Err("error 1"), Err("error 2"), Ok(4)]);
// try_join_all: First error propagates
let futures = vec![
async { Ok::<i32, &str>(1) },
async { Err("error 1") },
async { Err("error 2") },
async { Ok(4) },
];
let result: Result<Vec<i32>, &str> = try_join_all(futures).await;
// Only FIRST error is returned
assert_eq!(result, Err("error 1"));
// Other errors may never be seen
}With join_all you see all results; with try_join_all you see only the first error.
When Futures May Not Complete
use futures::future::{join_all, try_join_all};
async fn completion_guarantees() {
// join_all: ALL futures complete
// Even if some return Err, they complete
let futures = vec![
async { Ok::<i32, &str>(1) },
async { Err("fail") },
async { Ok(3) },
];
// All three futures complete, results are collected
// try_join_all: May NOT complete all futures
// First error causes immediate return
// Other futures continue running but their results are ignored
// This has implications:
// 1. Side effects may still occur from "ignored" futures
// 2. Resources may still be consumed
// 3. Cancellation safety matters
// If you need ALL futures to complete regardless of errors:
// Use join_all and handle errors in the Vec
}try_join_all may return before all futures complete; join_all waits for all.
Practical Example: Multiple HTTP Requests
use futures::future::{join_all, try_join_all};
// Simulated HTTP client
async fn fetch_url(url: &str) -> Result<String, String> {
// Simulate network request
if url.contains("error") {
Err(format!("Failed to fetch {}", url))
} else {
Ok(format!("Content from {}", url))
}
}
async fn http_example() {
let urls = vec!["page1", "page2", "error_page", "page3"];
// With join_all: All requests complete, collect all results
let futures: Vec<_> = urls.iter().map(|url| fetch_url(url)).collect();
let results: Vec<Result<String, String>> = join_all(futures).await;
// All 4 results present
assert_eq!(results.len(), 4);
for result in &results {
// Can inspect each success/failure
match result {
Ok(content) => println!("Success: {}", content),
Err(e) => println!("Failed: {}", e),
}
}
// With try_join_all: First error short-circuits
let futures: Vec<_> = urls.iter().map(|url| fetch_url(url)).collect();
let result: Result<Vec<String>, String> = try_join_all(futures).await;
// Returns Err("Failed to fetch error_page")
assert!(result.is_err());
// Requests for page1 and page2 completed
// Request for page3 may or may not have started
}Choose based on whether you need all results or need all to succeed.
Practical Example: Database Transactions
use futures::future::{join_all, try_join_all};
async fn database_example(db: &Db) -> Result<(), String> {
// Scenario: Update multiple records
// With try_join_all: All-or-nothing semantics
let updates: Vec<_> = records.iter()
.map(|r| db.update(r))
.collect();
// If ANY update fails, entire operation fails
let results = try_join_all(updates).await?;
// All updates succeeded
// Alternative with join_all: Collect results, decide what to do
let updates: Vec<_> = records.iter()
.map(|r| db.update(r))
.collect();
let results: Vec<Result<_, String>> = join_all(updates).await;
// Check for partial failures
let failures: Vec<_> = results.iter()
.filter(|r| r.is_err())
.collect();
if !failures.is_empty() {
// Handle partial failure scenario
// Maybe retry failed ones
// Maybe log and continue
}
}try_join_all for all-or-nothing; join_all for partial success handling.
The Empty Collection Edge Case
use futures::future::{join_all, try_join_all};
async fn empty_collection() {
// join_all: Empty vec returns empty vec
let futures: Vec<std::pin::Pin<Box<dyn futures::Future<Output = i32>>>> = vec![];
let results: Vec<i32> = join_all(futures).await;
assert_eq!(results, vec![]);
// try_join_all: Empty vec returns Ok(vec![])
let futures: Vec<std::pin::Pin<Box<dyn futures::Future<Output = Result<i32, String>>>>> = vec![];
let result: Result<Vec<i32>, String> = try_join_all(futures).await;
assert_eq!(result, Ok(vec![]));
// Both handle empty input gracefully
// try_join_all returns Ok(empty), not Err
}Both return successful empty results for empty input.
Error Type Requirements
use futures::future::try_join_all;
async fn error_type_requirements() {
// try_join_all requires all futures to have COMPATIBLE error types
// Specifically, all must be Result<T, E> for the SAME E
// Works: Same error type
let futures = vec![
async { Ok::<i32, String>(1) },
async { Ok::<i32, String>(2) },
];
let result: Result<Vec<i32>, String> = try_join_all(futures).await;
// Works: Different success types, same error type
let futures = vec![
async { Ok::<i32, String>(1) },
async { Ok::<&str, String>("hello") },
];
let result: Result<Vec<i32>, String> = try_join_all(futures).await;
// Wait, this won't compile - Vec must have single type
// Actually need to map to same type first
// Common pattern: Use Box<dyn Error> or custom error enum
let futures = vec![
async { Ok::<i32, Box<dyn std::error::Error>>(1) },
async { Ok::<i32, Box<dyn std::error::Error>>(2) },
];
let result: Result<Vec<i32>, Box<dyn std::error::Error>> = try_join_all(futures).await;
}All futures passed to try_join_all must have compatible error types.
Type Signatures Compared
use futures::future::{join_all, try_join_all};
use std::future::Future;
fn signatures() {
// join_all signature (simplified):
// fn join_all<I>(iter: I) -> JoinAll<I::Item>
// where
// I: IntoIterator,
// I::Item: Future<Output = T>, // Any output type
//
// Returns: impl Future<Output = Vec<T>>
// try_join_all signature (simplified):
// fn try_join_all<I>(iter: I) -> TryJoinAll<I::Item>
// where
// I: IntoIterator,
// I::Item: TryFuture, // Must be TryFuture (Result-based)
//
// Returns: impl Future<Output = Result<Vec<T>, E>>
// Key difference in trait bounds:
// join_all: Future<Output = T> (any output)
// try_join_all: TryFuture<Ok = T, Error = E> (Result output)
}The trait bounds enforce the different semantics at the type level.
Concurrency Behavior
use futures::future::{join_all, try_join_all};
use std::time::Duration;
use tokio::time::{sleep, Instant};
async fn concurrency() {
// BOTH run futures concurrently, not sequentially
// The "all" in the name means "wait for all", not "run all sequentially"
let start = Instant::now();
// join_all runs concurrently
let futures = vec![
async { sleep(Duration::from_millis(100)).await; 1 },
async { sleep(Duration::from_millis(100)).await; 2 },
async { sleep(Duration::from_millis(100)).await; 3 },
];
let _ = join_all(futures).await;
// Takes ~100ms total, not 300ms
// try_join_all also runs concurrently
let futures = vec![
async { sleep(Duration::from_millis(100)).await; Ok::<i32, ()>(1) },
async { sleep(Duration::from_millis(100)).await; Ok(2) },
async { sleep(Duration::from_millis(100)).await; Ok(3) },
];
let _ = try_join_all(futures).await;
// Also takes ~100ms total
// Both functions poll futures concurrently
// The difference is error handling, not concurrency
}Both functions execute futures concurrently; the difference is error handling, not execution model.
Combining with ? Operator
use futures::future::try_join_all;
async fn with_question_mark() -> Result<Vec<String>, String> {
// try_join_all works naturally with ?
let urls = vec!["url1", "url2", "url3"];
let futures: Vec<_> = urls.iter()
.map(|url| fetch_url(url))
.collect();
// ? propagates error immediately
let results = try_join_all(futures).await?;
// results is Vec<String> here
// All fetches succeeded
Ok(results)
// join_all does NOT work naturally with ?
// Because it returns Vec<Result<T, E>>, not Result<Vec<T>, E>
// You'd need to manually handle errors
}
async fn fetch_url(url: &str) -> Result<String, String> {
Ok(format!("Content from {}", url))
}try_join_all composes naturally with the ? operator; join_all does not.
Manual Error Collection with join_all
use futures::future::join_all;
async fn manual_error_handling() -> Result<Vec<String>, Vec<String>> {
let urls = vec!["url1", "error", "url3"];
let futures: Vec<_> = urls.iter()
.map(|url| fetch_url(url))
.collect();
// join_all returns Vec<Result<String, String>>
let results: Vec<Result<String, String>> = join_all(futures).await;
// Partition into successes and failures
let (successes, failures): (Vec<_>, Vec<_>) = results
.into_iter()
.partition(Result::is_ok);
if !failures.is_empty() {
// Return all failures
let errors: Vec<String> = failures
.into_iter()
.map(|r| r.unwrap_err())
.collect();
return Err(errors);
}
// All succeeded
let values: Vec<String> = successes
.into_iter()
.map(|r| r.unwrap())
.collect();
Ok(values)
}
async fn fetch_url(url: &str) -> Result<String, String> {
if url.contains("error") {
Err(format!("Failed: {}", url))
} else {
Ok(format!("Content: {}", url))
}
}join_all enables custom error aggregation; try_join_all propagates first error.
Summary Table
fn comparison_table() {
// | Aspect | join_all | try_join_all |
// |--------|----------|--------------|
// | Return type | Vec<T> | Result<Vec<T>, E> |
// | Future bound | Future<Output = T> | TryFuture<Ok = T, Error = E> |
// | Error handling | Collects errors as values | Short-circuits on first error |
// | Completion | Waits for all | May return early |
// | ? operator | Does NOT compose | Composes naturally |
// | Empty input | Returns vec![] | Returns Ok(vec![]) |
// | When to use | Function |
// |-------------|----------|
// | Need all results, even failures | join_all |
// | All-or-nothing semantics | try_join_all |
// | Want to count/aggregate errors | join_all |
// | Want early termination on error | try_join_all |
// | Partial success is acceptable | join_all |
// | Partial success is failure | try_join_all |
}Synthesis
Quick reference:
use futures::future::{join_all, try_join_all};
async fn quick_reference() {
// join_all: Collect all results, errors are values
let futures = vec![
async { Ok::<i32, &str>(1) },
async { Err("fail") },
async { Ok(3) },
];
let results: Vec<Result<i32, &str>> = join_all(futures).await;
// Returns: vec![Ok(1), Err("fail"), Ok(3)]
// try_join_all: First error propagates
let futures = vec![
async { Ok::<i32, &str>(1) },
async { Err("fail") },
async { Ok(3) },
];
let result: Result<Vec<i32>, &str> = try_join_all(futures).await;
// Returns: Err("fail")
}Key insight: join_all and try_join_all represent two fundamentally different approaches to handling failures in concurrent operations. join_all embraces the "collect everything" philosophyâall futures run to completion, all results are gathered, and errors are treated as values in the output vector. This is useful when you need to know about every failure, want to retry subsets, or need partial success handling. try_join_all embraces the "fail fast" philosophyâthe first error stops the operation and propagates immediately. This matches the semantics of the ? operator and is useful for all-or-nothing operations where partial success is equivalent to total failure. Both execute futures concurrentlyâthe difference is purely in how results are aggregated and errors are propagated. Choose join_all when you need all results; choose try_join_all when you need all to succeed.
