Loading page…
Rust walkthroughs
Loading page…
futures::future::join_all, what happens if one of the futures panics?When a future panics inside join_all, the panic propagates immediately, terminating the join_all operation. The remaining futures may not complete, and any futures that were being polled concurrently will be dropped. Unlike try_join_all which handles Err results gracefully, panics are not caught—they bubble up and abort the entire operation. This behavior reflects Rust's panic philosophy: panics are for unrecoverable errors and should not be silently caught.
use futures::future::join_all;
async fn basic_join_all() {
let futures = vec![
async { 1 },
async { 2 },
async { 3 },
];
// join_all waits for all futures and collects results
let results: Vec<i32> = join_all(futures).await;
println!("Results: {:?}", results); // [1, 2, 3]
}join_all runs all futures to completion and returns their results.
use futures::future::join_all;
use std::panic;
async fn panic_in_join_all() {
let futures = vec![
async { 1 },
async { panic!("future panicked!") },
async { 3 },
];
// This will panic! The panic propagates from join_all
let result = std::panic::catch_unwind(AssertUnwindSafe(|| {
futures::executor::block_on(async {
join_all(futures).await
})
}));
match result {
Ok(values) => println!("Completed: {:?}", values),
Err(panic_info) => println!("Panicked: {:?}", panic_info),
}
}
use std::panic::AssertUnwindSafe;The panic immediately propagates up from join_all.
use futures::future::join_all;
async fn immediate_propagation() {
let futures = vec![
async {
println!("Future 1 started");
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
println!("Future 1 completed");
1
},
async {
println!("Future 2 started");
panic!("Future 2 panicked!");
},
async {
println!("Future 3 started");
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
println!("Future 3 completed");
3
},
];
// When future 2 panics:
// 1. The panic propagates immediately
// 2. Futures 1 and 3 are dropped
// 3. Their sleep operations are cancelled
// Output might be:
// "Future 1 started"
// "Future 2 started"
// "Future 3 started"
// Then panic occurs
}Panics abort the entire join_all operation immediately.
use futures::future::{join_all, try_join_all};
async fn join_vs_try_join() {
// join_all: collects Results, panics propagate
let futures: Vec<_> = vec![
async { Ok::<_, String>(1) },
async { Err("error".to_string()) },
async { Ok(3) },
];
// join_all completes even with Err - it's just a value
let results: Vec<Result<i32, String>> = join_all(futures).await;
println!("join_all results: {:?}", results);
// [Ok(1), Err("error"), Ok(3)]
// try_join_all: short-circuits on first Err (but not panic)
let futures: Vec<_> = vec![
async { Ok::<_, String>(1) },
async { Err("error".to_string()) },
async { Ok(3) },
];
let result: Result<Vec<i32>, String> = try_join_all(futures).await;
println!("try_join_all result: {:?}", result);
// Err("error")
// Both propagate panics - neither catches panics
}try_join_all handles Err but not panics.
use futures::future::join_all;
async fn panic_vs_error() {
// Err is a value - join_all completes normally
let futures: Vec<_> = vec![
async { Ok::<i32, &str>(1) },
async { Err("an error") }, // This is just a value
async { Ok(3) },
];
let results = join_all(futures).await;
println!("With errors: {:?}", results);
// [Ok(1), Err("an error"), Ok(3)]
// Panic is not a value - join_all panics
let futures: Vec<_> = vec![
async { 1 },
async { panic!("not a value") }, // This panics
async { 3 },
];
// This await will panic
// let results = join_all(futures).await;
}Errors are values that join_all collects; panics abort the operation.
use futures::future::join_all;
use std::panic::{catch_unwind, AssertUnwindSafe};
async fn catching_panics() {
let futures = vec![
async { 1 },
async { panic!("panic in future!") },
async { 3 },
];
// catch_unwind can catch the panic
let result = catch_unwind(AssertUnwindSafe(|| {
futures::executor::block_on(join_all(futures))
}));
match result {
Ok(values) => println!("Success: {:?}", values),
Err(panic_payload) => {
if let Some(s) = panic_payload.downcast_ref::<&str>() {
println!("Caught panic: {}", s);
} else if let Some(s) = panic_payload.downcast_ref::<String>() {
println!("Caught panic: {}", s);
} else {
println!("Caught panic with unknown type");
}
}
}
}catch_unwind can catch panics at the boundary, but this is not idiomatic.
use futures::stream::{FuturesUnordered, StreamExt};
async fn futures_unordered_control() {
let mut futures = FuturesUnordered::new();
futures.push(async { 1 });
futures.push(async { panic!("panic!") });
futures.push(async { 3 });
// Process results as they complete
// But panics still propagate during polling
while let Some(result) = futures.next().await {
println!("Got result: {}", result);
// If a future panics, this loop aborts
}
}FuturesUnordered gives incremental results but panics still propagate.
use futures::future::{join_all, AbortHandle, Abortable, Aborted};
async fn with_abort_handles() {
let (abort_handle1, abort_reg1) = AbortHandle::new_pair();
let (abort_handle2, abort_reg2) = AbortHandle::new_pair();
let futures = vec![
Abortable::new(
async {
println!("Future 1 running");
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
println!("Future 1 done");
1
},
abort_reg1
),
Abortable::new(
async {
println!("Future 2 running");
panic!("Future 2 panicked!");
},
abort_reg2
),
];
// Still panics - Abortable doesn't catch panics
// But you can abort other futures if needed
}Abort handles don't catch panics, but allow explicit cancellation.
use futures::future::join_all;
#[tokio::main]
async fn tokio_panics() {
let futures = vec![
tokio::spawn(async { 1 }),
tokio::spawn(async { panic!("task panicked!") }),
tokio::spawn(async { 3 }),
];
// join_all on JoinHandles
let results: Vec<_> = join_all(futures).await;
// Results are Result<T, JoinError>
for (i, result) in results.into_iter().enumerate() {
match result {
Ok(value) => println!("Task {}: {:?}", i, value),
Err(e) => println!("Task {} failed: {}", i, e),
}
}
// The panic becomes a JoinError, not a propagated panic
// Task 0: 1
// Task 1 failed: task was cancelled (or panic message)
// Task 2: 3
}Tokio tasks convert panics to JoinError, which join_all collects.
use futures::future::join_all;
async fn safe_patterns() {
// Pattern 1: Use Result for expected failures
let futures: Vec<_> = vec![
async { Ok::<i32, String>(1) },
async { Err("expected failure".to_string()) },
async { Ok(3) },
];
let results: Vec<Result<i32, String>> = join_all(futures).await;
let successes: Vec<i32> = results.iter()
.filter_map(|r| r.as_ref().ok())
.copied()
.collect();
// Pattern 2: Handle panics at task boundary (Tokio)
let handles: Vec<_> = (0..3)
.map(|i| tokio::spawn(async move {
if i == 1 {
panic!("panic in task {}", i);
}
i
}))
.collect();
let results: Vec<_> = join_all(handles).await;
for (i, result) in results.into_iter().enumerate() {
match result {
Ok(v) => println!("Task {} succeeded: {}", i, v),
Err(e) => println!("Task {} failed: {}", i, e),
}
}
}Use Result for expected failures and task boundaries for panic isolation.
use futures::future::join_all;
async fn completion_order() {
let futures = vec![
async {
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
println!("Future 0 completed");
0
},
async {
tokio::time::sleep(std::time::Duration::from_millis(25)).await;
println!("Future 1 panicking");
panic!("Future 1!");
},
async {
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
println!("Future 2 completed");
2
},
];
// Possible execution:
// 1. All futures start
// 2. Future 1 panics after 25ms
// 3. Futures 0 and 2 are dropped
// 4. "Future 1 panicking" prints
// 5. Panic propagates
// "Future 0 completed" may never print
// "Future 2 completed" never prints
}Panics interrupt the normal completion order.
use futures::future::join_all;
struct DropLogger {
id: usize,
}
impl Drop for DropLogger {
fn drop(&mut self) {
println!("Dropping Future {}", self.id);
}
}
async fn drop_on_panic() {
let futures = vec![
async {
let _guard = DropLogger { id: 0 };
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
println!("Future 0 done");
},
async {
let _guard = DropLogger { id: 1 };
panic!("Future 1 panics!");
},
async {
let _guard = DropLogger { id: 2 };
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
println!("Future 2 done");
},
];
// When future 1 panics:
// 1. DropLogger 1 is dropped (normal scope exit)
// 2. Futures 0 and 2 are dropped
// 3. DropLogger 0 and 2 are dropped during unwinding
// Output:
// "Dropping Future 1"
// "Dropping Future 0"
// "Dropping Future 2"
// Then panic propagates
}Resources held by incomplete futures are dropped during unwinding.
use futures::future::join_all;
async fn panic_philosophy() {
// Rust's design: panics are unrecoverable errors
//
// join_all doesn't catch panics because:
// 1. Panics indicate programming bugs
// 2. Catching panics hides problems
// 3. State may be inconsistent after panic
// 4. The future that panicked may have left things broken
// For recoverable errors, use Result:
let futures: Vec<_> = vec![
async { fallible_operation().await },
async { fallible_operation().await },
];
let results: Vec<Result<_, _>> = join_all(futures).await;
// Handle errors explicitly
for result in results {
match result {
Ok(value) => println!("Success: {}", value),
Err(e) => println!("Error: {}", e),
}
}
}
async fn fallible_operation() -> Result<i32, String> {
Ok(42)
}Use Result for recoverable errors; let panics propagate for bugs.
use futures::future::join_all;
use std::panic::{catch_unwind, AssertUnwindSafe};
async fn join_all_with_panic_recovery<T: Send + 'static>(
futures: Vec<impl std::future::Future<Output = T> + Send + 'static>
) -> Vec<Option<T>> {
// Wrap each future to catch panics
let wrapped: Vec<_> = futures
.into_iter()
.map(|f| async {
// This doesn't work directly - catch_unwind is not async
// Need to use tokio::spawn or similar
// For demonstration, using a spawn-based approach
let handle = tokio::spawn(async move {
f.await
});
handle.await.ok()
})
.collect();
join_all(wrapped).await
}
// More realistic: wrap in spawned tasks
async fn safe_join_all_example() {
let futures: Vec<_> = (0..3)
.map(|i| async move {
if i == 1 {
panic!("panic in future {}", i);
}
i
})
.collect();
// Spawn each future as a task
let handles: Vec<_> = futures
.into_iter()
.map(|f| tokio::spawn(f))
.collect();
// join_all on handles - panics become JoinError
let results: Vec<_> = join_all(handles).await;
for (i, result) in results.into_iter().enumerate() {
match result {
Ok(value) => println!("Future {} returned {}", i, value),
Err(e) => println!("Future {} panicked: {}", i, e),
}
}
}Spawning futures as tasks isolates panics.
When a future panics inside join_all:
Immediate behavior:
join_all().awaitDropKey distinctions:
| Situation | join_all behavior |
|-----------|---------------------|
| Future returns Err | Collected as a value, all futures complete |
| Future panics | Panic propagates, other futures dropped |
| Tokio task panics | Becomes JoinError, collected as value |
Best practices:
Result for expected failures, not panicstry_join_all short-circuits on Err but still propagates panicscatch_unwind works but is not idiomatic for normal error handlingKey insight: join_all treats panics as truly exceptional—they abort the entire operation. This is intentional design: panics indicate bugs or unrecoverable states, not normal control flow. For recoverable errors, use Result return types and handle them explicitly.