Loading pageâŚ
Rust walkthroughs
Loading pageâŚ
{"args":{"content":"# How does futures::future::try_join differ from futures::future::join for error propagation in concurrent futures?
try_join waits for all futures to complete successfully and returns Err immediately when any future fails, while join waits for all futures regardless of success or failure and returns all results including errors. The key difference is error semantics: try_join short-circuits on the first error (similar to ? operator behavior), making it suitable when all results must succeed, while join collects all outcomes including failures, useful for cleanup or when you need to know about all failures. Use try_join when any failure should abort the operation, and join when you need to collect all results or perform cleanup regardless of individual failures.
use futures::future::join;
#[tokio::main]
async fn main() {
// join waits for all futures, returns tuple of results
let (a, b, c) = join(
async { 1u32 },
async { 2u32 },
async { 3u32 },
).await;
println!(\"a={}, b={}, c={}\", a, b, c);
// All three values available
}join returns a tuple with all results, waiting for every future to complete.
use futures::future::try_join;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// try_join returns Ok(tuple) if all succeed, Err on first failure
let (a, b, c) = try_join(
async { Ok::<_, Box<dyn std::error::Error>>(1u32) },
async { Ok::<_, Box<dyn std::error::Error>>(2u32) },
async { Ok::<_, Box<dyn std::error::Error>>(3u32) },
).await?;
println!(\"a={}, b={}, c={}\", a, b, c);
Ok(())
}try_join returns Result and short-circuits on error.
use futures::future::join;
#[tokio::main]
async fn main() {
async fn succeed() -> Result<&'static str, &'static str> {
println!(\"succeed started\");
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
println!(\"succeed done\");
Ok(\"success\")
}
async fn fail() -> Result<&'static str, &'static str> {
println!(\"fail started\");
tokio::time::sleep(std::time::Duration::from_millis(5)).await;
println!(\"fail done\");
Err(\"failed\")
}
// join waits for ALL futures, even when some fail
let (result1, result2, result3) = join(
succeed(),
fail(),
succeed(),
).await;
println!(\"Results:\");
println!(\" result1: {:?}\", result1);
println!(\" result2: {:?}\", result2);
println!(\" result3: {:?}\", result3);
// All three futures completed
// result2 is Err, but others still ran to completion
}join waits for all futures; failures don't affect other futures' execution.
use futures::future::try_join;
#[tokio::main]
async fn main() -> Result<(), &'static str> {
async fn succeed() -> Result<&'static str, &'static str> {
println!(\"succeed started\");
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
println!(\"succeed done\");
Ok(\"success\")
}
async fn fail_fast() -> Result<&'static str, &'static str> {
println!(\"fail started\");
Err(\"failed immediately\") // No delay
}
// try_join returns Err immediately when one fails
let result = try_join(
succeed(),
fail_fast(),
succeed(),
).await;
match result {
Ok((a, b, c)) => println!(\"All succeeded: {}, {}, {}\", a, b, c),
Err(e) => println!(\"Error: {}\", e),
}
// \"succeed started\" printed but succeed futures may not complete
// try_join returns as soon as failure is detected
Ok(())
}try_join returns immediately on error; other futures continue running but their results are discarded.
use futures::future::{join, try_join};
#[tokio::main]
async fn main() {
// join returns tuple of Results
let result: (Result<i32, &str>, Result<i32, &str>) = join(
async { Ok::<i32, &str>(1) },
async { Ok::<i32, &str>(2) },
).await;
// Result is inside each tuple element
// try_join returns Result of tuple
let result: Result<(i32, i32), &str> = try_join(
async { Ok::<i32, &str>(1) },
async { Ok::<i32, &str>(2) },
).await;
// Result wraps the entire tuple
// join: (Result<T, E>, Result<T, E>)
// try_join: Result<(T, T), E>
}join returns (Result, Result); try_join returns Result<(T, T), E>.
use futures::future::try_join;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// try_join requires all error types to be compatible
// All futures must return Result<T, E> where E is the same type
// Works: same error type
let (a, b) = try_join(
async { Ok::<i32, Box<dyn std::error::Error>>(1) },
async { Ok::<i32, Box<dyn std::error::Error>>(2) },
).await?;
// Different error types need conversion
let (c, d) = try_join(
async { Ok::<i32, &str>(1).map_err(|e| e.to_string()) },
async { Ok::<i32, String>(2) },
).await?;
println!(\"a={}, b={}, c={}, d={}\", a, b, c, d);
Ok(())
}try_join requires all error types to match; join doesn't care.
use futures::future::join;
#[tokio::main]
async fn main() {
async fn task_with_cleanup(id: u32) -> Result<String, String> {
println!(\"Task {} started\", id);
// Simulate work
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
// Simulate failure
if id == 2 {
println!(\"Task {} cleaning up after error\", id);
return Err(format!(\"Task {} failed\", id));
}
println!(\"Task {} cleaning up after success\", id);
Ok(format!(\"Task {} result\", id))
}
// join ensures all cleanup runs
let results = join(
task_with_cleanup(1),
task_with_cleanup(2),
task_with_cleanup(3),
).await;
// All three tasks complete their cleanup
// Even though task 2 failed
for (i, result) in results.iter().enumerate() {
println!(\"Task {}: {:?}\", i + 1, result);
}
}join ensures all futures complete; useful for cleanup operations.
use futures::future::try_join;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
async fn write_database(data: &str) -> Result<u64, Box<dyn std::error::Error>> {
println!(\"Writing to database: {}\", data);
// Simulate database write
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
Ok(data.len() as u64)
}
async fn write_cache(data: &str) -> Result<bool, Box<dyn std::error::Error>> {
println!(\"Writing to cache: {}\", data);
// Simulate cache write
tokio::time::sleep(std::time::Duration::from_millis(5)).await;
Ok(true)
}
async fn write_storage(data: &str) -> Result<String, Box<dyn std::error::Error>> {
println!(\"Writing to storage: {}\", data);
// Simulate failure
Err(\"Storage write failed\".into())
}
// try_join fails fast on any error
let result = try_join(
write_database(\"data\"),
write_cache(\"data\"),
write_storage(\"data\"),
).await;
match result {
Ok(_) => println!(\"All writes succeeded\"),
Err(e) => {
println!(\"Transaction failed: {}\", e);
// Note: Some writes may have completed
// try_join doesn't roll back completed operations
}
}
Ok(())
}try_join short-circuits but doesn't automatically roll back completed operations.
use futures::future::join;
#[tokio::main]
async fn main() {
async fn validate_field(name: &str, value: &str) -> Result<(), String> {
if value.is_empty() {
Err(format!(\"{} is empty\", name))
} else if value.len() < 3 {
Err(format!(\"{} is too short\", name))
} else {
Ok(())
}
}
// join all validations to see all errors
let (name_result, email_result, age_result) = join(
validate_field(\"name\", \"\"),
validate_field(\"email\", \"a\"),
validate_field(\"age\", \"valid\"),
).await;
// Collect all errors
let errors: Vec<String> = [
name_result.err(),
email_result.err(),
age_result.err(),
]
.into_iter()
.flatten()
.collect();
for error in &errors {
println!(\"Validation error: {}\", error);
}
// Shows: \"name is empty\" and \"email is too short\"
}join lets you collect all errors; try_join would only show the first.
use futures::future::try_join;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// For network requests where any failure is fatal
async fn fetch_user(id: u32) -> Result<String, Box<dyn std::error::Error>> {
// Simulate API call
if id == 0 {
Err(\"Invalid user ID\".into())
} else {
Ok(format!(\"User {}\", id))
}
}
async fn fetch_posts(user_id: u32) -> Result<Vec<String>, Box<dyn std::error::Error>> {
Ok(vec![format!(\"Post 1 by {}\", user_id)])
}
async fn fetch_comments(user_id: u32) -> Result<Vec<String>, Box<dyn std::error::Error>> {
Ok(vec![format!(\"Comment for {}\", user_id)])
}
// try_join: if user fetch fails, don't bother with posts/comments
let user = fetch_user(1).await?;
let (posts, comments) = try_join(
fetch_posts(1),
fetch_comments(1),
).await?;
println!(\"User: {}\", user);
println!(\"Posts: {:?}\", posts);
println!(\"Comments: {:?}\", comments);
Ok(())
}Use try_join when subsequent operations depend on previous success.
use futures::future::join;
#[tokio::main]
async fn main() {
async fn process_chunk(id: u32, data: Vec<i32>) -> Result<u32, String> {
println!(\"Processing chunk {}\", id);
if id == 2 {
return Err(format!(\"Chunk {} failed\", id));
}
Ok(data.len() as u32)
}
let chunks: Vec<Vec<i32>> = vec![
vec![1, 2, 3],
vec![4, 5, 6],
vec![7, 8, 9],
];
// Process all chunks in parallel with join
let results = join(
process_chunk(1, chunks[0].clone()),
process_chunk(2, chunks[1].clone()),
process_chunk(3, chunks[2].clone()),
).await;
// Check which succeeded
let successful: Vec<u32> = [results.0.clone(), results.1.clone(), results.2.clone()]
.iter()
.filter_map(|r| r.as_ref().ok().copied())
.collect();
println!(\"Successful chunks: {:?}\", successful);
let failed: Vec<&str> = [results.0.as_ref(), results.1.as_ref(), results.2.as_ref()]
.iter()
.filter_map(|r| r.as_ref().err().map(|s| *s))
.collect();
println!(\"Failed chunks: {:?}\", failed);
}join allows partial success handling; try_join is all-or-nothing.
use futures::future::{join, try_join};
use std::time::Instant;
#[tokio::main]
async fn main() {
async fn slow_task(id: u32) -> Result<u32, ()> {
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
println!(\"Slow task {} completed\", id);
Ok(id)
}
async fn fast_fail() -> Result<u32, ()> {
println!(\"Fast fail completed\");
Err(())
}
// join: waits for all tasks even if one fails
let start = Instant::now();
let _ = join(
slow_task(1),
fast_fail(),
slow_task(2),
).await;
println!(\"join took: {:?}\", start.elapsed());
// All three tasks complete, ~100ms total
// try_join: returns as soon as one fails
let start = Instant::now();
let _ = try_join(
slow_task(1),
fast_fail(),
slow_task(2),
).await;
println!(\"try_join took: {:?}\", start.elapsed());
// Returns immediately after fast_fail completes
// Other tasks continue running but results are discarded
}try_join can return faster on failure; join always waits for all.
use futures::future::{join_all, try_join_all};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// join_all: Vec of Results
let results: Vec<Result<i32, &str>> = join_all(vec![
async { Ok::<i32, &str>(1) },
async { Ok::<i32, &str>(2) },
async { Err::<i32, &str>(\"error\") },
]).await;
println!(\"join_all results: {:?}\", results);
// try_join_all: Result of Vec
let result: Result<Vec<i32>, &str> = try_join_all(vec![
async { Ok::<i32, &str>(1) },
async { Ok::<i32, &str>(2) },
]).await;
println!(\"try_join_all result: {:?}\", result);
// try_join_all fails on first error
let result: Result<Vec<i32>, &str> = try_join_all(vec![
async { Ok::<i32, &str>(1) },
async { Err::<i32, &str>(\"error\") },
async { Ok::<i32, &str>(3) }, // Not awaited
]).await;
println!(\"try_join_all with error: {:?}\", result); // Err(\"error\")
Ok(())
}join_all and try_join_all work with collections of futures.
use futures::future::try_join;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
async fn cancellable_task(id: u32, cancel: tokio::sync::watch::Receiver<bool>)
-> Result<String, Box<dyn std::error::Error>>
{
println!(\"Task {} started\", id);
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
if *cancel.borrow() {
println!(\"Task {} cancelled\", id);
return Err(format!(\"Task {} cancelled\", id).into());
}
println!(\"Task {} completed\", id);
Ok(format!(\"Result {}\", id))
}
// try_join doesn't cancel other futures when one fails
// The other futures continue running to completion
// Only the returned Result is affected
let (tx, rx) = tokio::sync::watch::channel(false);
let result = try_join(
async { Err::<String, Box<dyn std::error::Error>>(\"immediate error\".into()) },
async {
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
println!(\"Second task still running\");
Ok(\"second\".to_string())
},
).await;
println!(\"Result: {:?}\", result);
// Second task still prints \"Second task still running\"
Ok(())
}try_join doesn't cancel running futures; it just returns early.
use futures::future::{join, try_join};
// Use join when:
// - You need all results regardless of success/failure
// - Cleanup operations must complete
// - Collecting all errors for validation
// - Independent operations that should all run to completion
// - You want to handle partial success
// Use try_join when:
// - Any failure should abort the logical operation
// - Subsequent operations depend on all succeeding
// - Short-circuit on error is desired
// - All-or-nothing semantics required
// - Want to return early on first error
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Example: Parallel validation (need all errors)
// Use join
// Example: Parallel database writes (all must succeed)
// Use try_join
// Example: Parallel API calls where any failure is fatal
// Use try_join
// Example: Parallel cleanup operations
// Use join
// Example: Parallel downloads with fallback handling
// Use join to see which succeeded/failed
// Example: Transaction-like operations
// Use try_join (though note: no automatic rollback)
Ok(())
}Choose based on whether partial success should be visible.
join characteristics:
(Result<T, E>, Result<T, E>, ...)Future typetry_join characteristics:
Result<(T, T, ...), E>Err)ResultReturn types:
join(f1, f2).await â (F1::Output, F2::Output)try_join(f1, f2).await â Result<(T1, T2), E>Use join when:
Use try_join when:
Important caveat:
Neither join nor try_join cancels running futures. When try_join returns Err, the other futures continue running in the backgroundâthey're not cancelled. Their results are just discarded. If you need cancellation, use tokio::select! or explicit cancellation tokens.
Key insight: The fundamental difference is error visibility and flow control. join gives you visibility into all outcomesâyou see every success and every failure. try_join gives you short-circuit semanticsâfail fast and handle the first error. This mirrors the difference between collecting all validation errors (use join) versus failing on the first error (use try_join). The choice depends on whether partial success is meaningful in your domain: if you need to know about all failures or process successful results independently, use join; if any failure makes the entire operation invalid, use try_join.","path":"/articles/290_futures_try_join_vs_join.md"},"tool_call":"file.create"}