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.