Introduction

Rayon's parallel iterators distribute work across multiple threads. When one element's processing panics, the panic propagates to the calling thread and aborts the entire parallel iteration. All in-progress work on other threads is cancelled, and partial results are lost. This can be particularly problematic in data processing pipelines where some elements may be malformed but the majority should still be processed.

Symptoms

  • thread panicked at 'index out of bounds' in a par_iter() call
  • Entire parallel iteration aborts due to a single element's panic
  • Partial results from other threads are discarded
  • Panic message does not identify which element caused the failure
  • catch_unwind does not work inside par_iter closures directly

Example error: `` thread 'thread 3' panicked at 'index out of bounds: the len is 5 but the index is 10', src/processor.rs:42:15 note: run with RUST_BACKTRACE=1 environment variable to display a backtrace

Common Causes

  • Data inconsistency: some elements have invalid state that triggers panic
  • Off-by-one errors that only manifest on certain elements under parallel execution
  • External service calls that panic on specific inputs
  • Division by zero or arithmetic overflow on specific data values
  • Assertion failures that trigger only for edge cases

Step-by-Step Fix

  1. 1.**Use filter_map to handle errors instead of panicking**:
  2. 2.```rust
  3. 3.use rayon::prelude::*;

// Before: panics on bad elements let results: Vec<_> = items.par_iter() .map(|item| process_item(item).unwrap()) .collect();

// After: collect errors alongside results let (results, errors): (Vec<_>, Vec<_>) = items.par_iter() .map(|item| process_item(item)) .partition_map(|result| match result { Ok(value) => rayon::Either::Left(value), Err(e) => rayon::Either::Right(e), });

eprintln!("Processed {} items, {} errors", results.len(), errors.len()); ```

  1. 1.**Use panic::catch_unwind inside parallel iteration**:
  2. 2.```rust
  3. 3.use std::panic;
  4. 4.use rayon::prelude::*;

let results: Vec<_> = items.par_iter() .filter_map(|item| { let item = item.clone(); panic::catch_unwind(panic::AssertUnwindSafe(|| { process_item(&item) })).ok() // Convert panic to None }) .collect(); ```

  1. 1.Identify the problematic element:
  2. 2.```rust
  3. 3.let results: Vec<_> = items.par_iter()
  4. 4..enumerate()
  5. 5..filter_map(|(i, item)| {
  6. 6.let result = panic::catch_unwind(panic::AssertUnwindSafe(|| {
  7. 7.process_item(item)
  8. 8.}));

match result { Ok(value) => Some(Ok(value)), Err(panic_info) => { eprintln!("Panic at index {}: {:?}", i, panic_info); Some(Err((i, panic_info))) } } }) .collect(); ```

  1. 1.**Use par_bridge for fallible iterators**:
  2. 2.```rust
  3. 3.use rayon::prelude::*;
  4. 4.use std::sync::Mutex;

// For operations that can fail, collect errors let errors = Mutex::new(Vec::new()); let results: Vec<_> = items.par_iter() .filter_map(|item| { match std::panic::catch_unwind(panic::AssertUnwindSafe(|| { process_item(item) })) { Ok(Ok(value)) => Some(value), Ok(Err(e)) => { errors.lock().unwrap().push(e); None } Err(panic) => { errors.lock().unwrap().push(format!("Panic: {:?}", panic)); None } } }) .collect(); ```

Prevention

  • Never use .unwrap() or .expect() inside par_iter() closures
  • Use Result types and filter_map to handle errors gracefully
  • Use catch_unwind for external code that may panic
  • Validate data before entering parallel iteration
  • Add logging that identifies which element is being processed
  • Test parallel code with edge cases using proptest or quickcheck