Loading pageā¦
Rust walkthroughs
Loading pageā¦
futures::future::join_all handle task failures compared to try_join_all?futures::future::join_all and try_join_all both run multiple futures concurrently to completion, but they differ fundamentally in failure semantics: join_all waits for all futures to complete regardless of failures, returning a Vec<Result> where each element reflects individual success or error, while try_join_all fails fast on the first error, returning Result<Vec<T>, E> where the entire operation fails if any future errors. This distinction determines whether you need all results regardless of partial failures (join_all) or require complete success (try_join_all), with significant implications for error handling patterns, resource cleanup, and API design in concurrent systems.
use futures::future::join_all;
#[tokio::main]
async fn main() {
let futures = vec
![
async { 1 },
async { 2 },
async { 3 },
];
// join_all returns Vec<T> - all results
let results: Vec<i32> = join_all(futures).await;
println!("Results: {:?}", results); // [1, 2, 3]
}join_all returns a Vec containing all results in order, waiting for every future.
use futures::future::try_join_all;
#[tokio::main]
async fn main() {
let futures = vec
![
async { Ok::<_, String>(1) },
async { Ok(2) },
async { Ok(3) },
];
// try_join_all returns Result<Vec<T>, E>
let result: Result<Vec<i32>, String> = try_join_all(futures).await;
println!("Result: {:?}", result); // Ok([1, 2, 3])
}try_join_all returns Result<Vec<T>, E>, failing immediately if any future errors.
use futures::future::join_all;
#[tokio::main]
async fn main() {
let futures = vec
![
async { Ok::<_, String>(1) },
async { Err::<i32, String>("error at 2".to_string()) },
async { Ok(3) },
async { Err("error at 4".to_string()) },
];
// join_all completes ALL futures, collecting results
let results: Vec<Result<i32, String>> = join_all(futures).await;
for (i, result) in results.iter().enumerate() {
match result {
Ok(v) => println!("Future {}: Ok({})", i, v),
Err(e) => println!("Future {}: Err({})", i, e),
}
}
// Output:
// Future 0: Ok(1)
// Future 1: Err(error at 2)
// Future 2: Ok(3)
// Future 3: Err(error at 4)
}join_all collects all results including errors; every future runs to completion.
use futures::future::try_join_all;
#[tokio::main]
async fn main() {
let futures = vec
![
async { Ok::<_, String>(1) },
async { Err::<i32, String>("error at 2".to_string()) },
async { Ok(3) },
];
// try_join_all fails fast - returns first error
let result: Result<Vec<i32>, String> = try_join_all(futures).await;
match result {
Ok(values) => println!("All succeeded: {:?}", values),
Err(e) => println!("Failed: {}", e), // This prints
}
}try_join_all returns the first error encountered; other futures may still run.
use futures::future::try_join_all;
use std::time::{Duration, Instant};
#[tokio::main]
async fn main() {
let start = Instant::now();
let futures = vec
![
async {
tokio::time::sleep(Duration::from_millis(100)).await;
println!("Fast future completed at {:?}", start.elapsed());
Ok::<_, String>(1)
},
async {
tokio::time::sleep(Duration::from_millis(50)).await;
println!("Error future failing at {:?}", start.elapsed());
Err::<i32, String>("immediate error".to_string())
},
async {
tokio::time::sleep(Duration::from_millis(200)).await;
println!("Slow future completed at {:?}", start.elapsed());
Ok(3)
},
];
let result = try_join_all(futures).await;
println!("Result: {:?}, Total time: {:?}", result, start.elapsed());
// Note: try_join_all does NOT cancel other futures on error
// All futures continue running until completion
// Output order varies:
// Error future failing at ~50ms
// Result: Err("immediate error"), Total time: ~50ms
// But other futures still complete:
// Fast future completed at ~100ms
// Slow future completed at ~200ms
}try_join_all returns early on error but does not automatically cancel running futures.
use futures::future::join_all;
use std::time::{Duration, Instant};
#[tokio::main]
async fn main() {
let start = Instant::now();
let futures = vec
![
async {
tokio::time::sleep(Duration::from_millis(100)).await;
println!("Fast completed at {:?}", start.elapsed());
Ok::<_, String>(1)
},
async {
tokio::time::sleep(Duration::from_millis(50)).await;
println!("Error completed at {:?}", start.elapsed());
Err::<i32, String>("error".to_string())
},
async {
tokio::time::sleep(Duration::from_millis(200)).await;
println!("Slow completed at {:?}", start.elapsed());
Ok(3)
},
];
let results: Vec<Result<i32, String>> = join_all(futures).await;
println!("Completed at {:?}, results: {:?}", start.elapsed(), results);
// All futures complete:
// Error completed at ~50ms
// Fast completed at ~100ms
// Slow completed at ~200ms
// Completed at ~200ms
}join_all always waits for all futures; total time is the longest future.
use futures::future::{join_all, try_join_all};
#[tokio::main]
async fn main() {
// join_all: Vec<Result<T, E>>
// Each result independent
let futures1 = vec
![
async { Ok::<_, String>(1) },
async { Err::<i32, String>("error") },
];
let results1: Vec<Result<i32, String>> = join_all(futures1).await;
// Type: Vec<Result<i32, String>>
// Contains both Ok and Err values
// try_join_all: Result<Vec<T>, E>
// All succeed or first error
let futures2 = vec
![
async { Ok::<_, String>(1) },
async { Ok(2) },
];
let results2: Result<Vec<i32>, String> = try_join_all(futures2).await;
// Type: Result<Vec<i32>, String>
// Either all values or one error
println!("join_all: {:?}", results1);
println!("try_join_all: {:?}", results2);
}The return types reflect the semantics: Vec<Result> vs Result<Vec>.
use futures::future::join_all;
#[derive(Debug)]
struct TaskResult {
id: usize,
success: bool,
data: Option<String>,
}
async fn run_task(id: usize) -> TaskResult {
// Simulate some tasks failing
if id % 3 == 0 {
TaskResult { id, success: false, data: None }
} else {
TaskResult {
id,
success: true,
data: Some(format!("data-{}", id))
}
}
}
#[tokio::main]
async fn main() {
let futures: Vec<_> = (0..5).map(run_task).collect();
// Get all results, successful or not
let results: Vec<TaskResult> = join_all(futures).await;
let successful: Vec<_> = results.iter()
.filter(|r| r.success)
.collect();
let failed: Vec<_> = results.iter()
.filter(|r| !r.success)
.collect();
println!("Successful: {:?}", successful.len());
println!("Failed: {:?}", failed.len());
println!("Total: {:?}", results.len());
// Can process partial results
for result in &results {
if result.success {
println!("Task {} succeeded: {:?}", result.id, result.data);
} else {
println!("Task {} failed", result.id);
}
}
}Use join_all when partial results are valuable; you can filter successes later.
use futures::future::try_join_all;
#[derive(Debug)]
struct ValidationError(String);
async fn validate_field(name: &str, value: &str) -> Result<String, ValidationError> {
if value.is_empty() {
Err(ValidationError(format!("{} cannot be empty", name)))
} else {
Ok(format!("{}: {}", name, value))
}
}
#[tokio::main]
async fn main() {
// Validation should fail if ANY field is invalid
let validations = vec
![
validate_field("name", "Alice"),
validate_field("email", "alice@example.com"),
validate_field("phone", ""), // Invalid!
];
match try_join_all(validations).await {
Ok(valid_fields) => {
println!("All fields valid: {:?}", valid_fields);
}
Err(ValidationError(msg)) => {
println!("Validation failed: {}", msg);
}
}
// If all fields are valid:
let valid_validations = vec
![
validate_field("name", "Bob"),
validate_field("email", "bob@example.com"),
validate_field("phone", "555-1234"),
];
match try_join_all(valid_validations).await {
Ok(fields) => println!("All fields valid: {:?}", fields),
Err(e) => println!("Validation failed: {:?}", e),
}
}Use try_join_all when all futures must succeed for the operation to be valid.
use futures::future::join_all;
#[derive(Debug)]
struct AggregateError {
errors: Vec<String>,
}
async fn fetch_url(url: &str) -> Result<String, String> {
// Simulate some URLs failing
if url.contains("error") {
Err(format!("Failed to fetch {}", url))
} else {
Ok(format!("Content from {}", url))
}
}
#[tokio::main]
async fn main() {
let urls = vec
!["http://ok.com", "http://error.com", "http://fine.com"];
let futures: Vec<_> = urls.iter().map(|url| fetch_url(url)).collect();
let results: Vec<Result<String, String>> = join_all(futures).await;
// Collect all errors
let errors: Vec<_> = results.iter()
.filter_map(|r| r.as_ref().err().cloned())
.collect();
let successes: Vec<_> = results.iter()
.filter_map(|r| r.as_ref().ok().cloned())
.collect();
println!("Successful: {:?}", successes.len());
println!("Failed: {:?}", errors.len());
if !errors.is_empty() {
let aggregate = AggregateError { errors };
println!("Aggregate error: {:?}", aggregate);
}
}join_all enables collecting all errors rather than stopping at the first.
use futures::future::{join_all, try_join_all};
use tokio_util::sync::CancellationToken;
#[tokio::main]
async fn main() {
// Manual cancellation for try_join_all
let token = CancellationToken::new();
let futures = vec
![
async {
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
println!("Task 1 completed");
Ok::<_, String>(1)
},
async {
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
println!("Task 2 failing");
Err::<i32, String>("Task 2 error".to_string())
},
async {
// This would run for a long time
tokio::time::sleep(std::time::Duration::from_secs(10)).await;
println!("Task 3 completed");
Ok(3)
},
];
// try_join_all doesn't cancel, but we can wrap with select
tokio::select! {
result = try_join_all(futures) => {
println!("Result: {:?}", result);
}
_ = tokio::time::sleep(std::time::Duration::from_millis(200)) => {
println!("Timeout - some futures still running");
}
}
println!("Done (but Task 3 may still be running in background)");
}Neither join_all nor try_join_all automatically cancels other futures.
use futures::future::{join_all, try_join_all};
#[tokio::main]
async fn main() {
// Empty join_all returns empty Vec
let empty_futures: Vec<std::future::Ready<i32>> = vec
![];
let results: Vec<i32> = join_all(empty_futures).await;
println!("join_all empty: {:?}", results); // []
// Empty try_join_all returns Ok(Vec::new())
let empty_try_futures: Vec<std::future::Ready<Result<i32, String>>> = vec
![];
let result: Result<Vec<i32>, String> = try_join_all(empty_try_futures).await;
println!("try_join_all empty: {:?}", result); // Ok([])
}Both handle empty collections gracefully; try_join_all returns Ok([]).
use futures::future::try_join_all;
use std::time::Duration;
#[derive(Debug)]
struct ApiResponse {
endpoint: String,
data: String,
}
async fn fetch_endpoint(endpoint: String) -> Result<ApiResponse, String> {
// Simulate API call
tokio::time::sleep(Duration::from_millis(100)).await;
if endpoint.contains("fail") {
Err(format!("Endpoint {} failed", endpoint))
} else {
Ok(ApiResponse {
endpoint: endpoint.clone(),
data: format!("Data from {}", endpoint),
})
}
}
#[tokio::main]
async fn main() {
// Scenario: fetch from multiple endpoints
// If any fails, the whole operation fails
let endpoints = vec
![
"api/users".to_string(),
"api/posts".to_string(),
"api/comments".to_string(),
];
let futures: Vec<_> = endpoints
.into_iter()
.map(fetch_endpoint)
.collect();
match try_join_all(futures).await {
Ok(responses) => {
println!("All {} endpoints fetched successfully", responses.len());
for resp in responses {
println!(" {}: {}", resp.endpoint, resp.data);
}
}
Err(e) => {
println!("Failed to fetch all endpoints: {}", e);
}
}
}Use try_join_all when all requests must succeed for consistent state.
use futures::future::join_all;
#[derive(Debug)]
struct PartialResult {
successful: Vec<String>,
failed: Vec<String>,
}
async fn fetch_best_effort(urls: Vec<&str>) -> PartialResult {
let futures: Vec<_> = urls.iter()
.map(|url| async {
// Simulate fetch
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
if url.contains("error") {
Err(format!("Failed: {}", url))
} else {
Ok(format!("Content: {}", url))
}
})
.collect();
let results: Vec<Result<String, String>> = join_all(futures).await;
let successful: Vec<String> = results.iter()
.filter_map(|r| r.as_ref().ok().cloned())
.collect();
let failed: Vec<String> = results.iter()
.filter_map(|r| r.as_ref().err().cloned())
.collect();
PartialResult { successful, failed }
}
#[tokio::main]
async fn main() {
let urls = vec
!["ok1.com", "error.com", "ok2.com", "fail.com", "ok3.com"];
let result = fetch_best_effort(urls).await;
println!("Successful: {:?}", result.successful);
println!("Failed: {:?}", result.failed);
// Application continues with partial data
}Use join_all when partial results are acceptable and useful.
Behavior comparison:
| Aspect | join_all | try_join_all |
|--------|-----------|----------------|
| Return type | Vec<Result<T, E>> | Result<Vec<T>, E> |
| On error | Continues, collects all | Returns first error |
| Completion | Waits for all | Returns on first error |
| Empty input | Vec::new() | Ok(Vec::new()) |
| Use case | Partial results OK | All must succeed |
Error semantics:
| Pattern | Function | Error Handling |
|---------|----------|----------------|
| Best-effort collection | join_all | Process successes, log errors |
| All-or-nothing transaction | try_join_all | Fail entire operation |
| Error aggregation | join_all | Collect all errors for reporting |
| Fast failure | try_join_all | Return immediately on first error |
Key insight: join_all and try_join_all represent two philosophies of concurrent error handling. join_all treats errors as dataāevery future completes, and you receive a Vec<Result> where each element tells you individually whether that particular future succeeded or failed. This is valuable for best-effort operations where partial results are meaningful: fetching multiple URLs where some may timeout, processing multiple files where some may be corrupted, or validating multiple inputs where you want to report all errors at once. try_join_all treats errors as operation failuresāif any future errors, the entire try_join_all returns that error as Err(E). This is essential for transactional semantics: database operations where all must commit or none, validation where any failure invalidates the whole, or request patterns where you need consistent success. Importantly, neither function cancels running futures on errorāthe futures continue executing until they complete naturally. For cancellation semantics, you need explicit patterns using select! or CancellationToken. Choose join_all when you want all results and will handle errors per-future; choose try_join_all when you need all futures to succeed for the operation to be valid.