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_all returns Vec<T> after all futures complete
  • try_join_all returns Result<Vec<T>, E> and short-circuits on first error

Return types:

  • join_all: Vec<T> where T is the future's output (can be Result)
  • try_join_all: Result<Vec<T>, E> where futures must return Result<T, E>

Error handling:

  • join_all: All futures complete; collect successes and failures separately
  • try_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 Result propagation 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.