Descriptive Messages
Include item IDs, file names, line numbers in error messages
Ce contenu n’est pas encore disponible dans votre langue.
Spring Batch RS uses the BatchError enum for all error conditions. This guide covers all error types and handling strategies.
pub enum BatchError { ItemReader(String), ItemProcessor(String), ItemWriter(String), Tasklet(String), Io(std::io::Error), Database(String), Configuration(String), Validation(String), Fatal(String),}When: Reading data from sources fails
Common Causes:
Example:
impl ItemReader<Record> for MyReader { fn read(&self) -> ItemReaderResult<Record> { // File not found Err(BatchError::ItemReader( "Could not open file: data.csv".to_string() ))
// Parse error Err(BatchError::ItemReader( format!("Invalid CSV format at line {}", line_num) ))
// Database error Err(BatchError::ItemReader( format!("Query failed: {}", db_error) )) }}Best Practices:
When: Item transformation or validation fails
Common Causes:
Example:
impl ItemProcessor<Input, Output> for MyProcessor { fn process(&self, item: &Input) -> ItemProcessorResult<Output> { // Validation error if item.age < 0 { return Err(BatchError::ItemProcessor( format!("Invalid age: {} for item {}", item.age, item.id) )); }
// Parse error let value = item.price.parse::<f64>() .map_err(|e| BatchError::ItemProcessor( format!("Cannot parse price '{}': {}", item.price, e) ))?;
// Business rule violation if value > 1000.0 && !item.approved { return Err(BatchError::ItemProcessor( "High-value items require approval".to_string() )); }
Ok(Output { value, /* ... */ }) }}Handling Strategy:
let step = StepBuilder::new("validate") .chunk::<Input, Output>(100) .reader(&reader) .processor(&processor) .writer(&writer) .skip_limit(10) // Skip up to 10 processor errors .build();When: Writing data to destinations fails
Common Causes:
Example:
impl ItemWriter<Record> for MyWriter { fn write(&self, items: &[Record]) -> ItemWriterResult { // Disk space error Err(BatchError::ItemWriter( "Insufficient disk space".to_string() ))
// Permission error Err(BatchError::ItemWriter( format!("Cannot write to {}: Permission denied", path) ))
// Database constraint Err(BatchError::ItemWriter( format!("Duplicate key violation: {}", key) ))
// Transaction error Err(BatchError::ItemWriter( format!("Transaction failed: {}", db_error) )) }
fn close(&self) -> ItemWriterResult { // Cleanup error Err(BatchError::ItemWriter( "Failed to flush buffers".to_string() )) }}Best Practices:
When: Single-task operations fail
Common Causes:
Example:
impl Tasklet for MyTasklet { fn execute(&self, _step_execution: &StepExecution) -> Result<RepeatStatus, BatchError> { // File operation error fs::copy(src, dst) .map_err(|e| BatchError::Tasklet( format!("Failed to copy {} to {}: {}", src, dst, e) ))?;
// Command execution error let output = Command::new("zip") .arg("archive.zip") .arg("data/") .output() .map_err(|e| BatchError::Tasklet( format!("Zip command failed: {}", e) ))?;
if !output.status.success() { return Err(BatchError::Tasklet( format!("Zip failed: {}", String::from_utf8_lossy(&output.stderr)) )); }
// Network operation error ftp_client.upload(file) .map_err(|e| BatchError::Tasklet( format!("FTP upload failed: {}", e) ))?;
Ok(RepeatStatus::Finished) }}When: File system operations fail
Automatically Converted From: std::io::Error
Common Causes:
Example:
use std::fs::File;
// Automatic conversionlet file = File::open("data.csv")?; // io::Error -> BatchError::Io
// Manual conversionlet file = File::open("data.csv") .map_err(|e| BatchError::Io(e))?;
// With contextlet file = File::open(&path) .map_err(|e| BatchError::ItemReader( format!("Cannot open {}: {}", path, e) ))?;When: Database operations fail
Common Causes:
Example:
// Query errorsqlx::query("SELECT * FROM users") .fetch_all(&pool) .await .map_err(|e| BatchError::Database( format!("Query failed: {}", e) ))?;
// Connection errorPgPool::connect(&url) .await .map_err(|e| BatchError::Database( format!("Cannot connect to database: {}", e) ))?;
// Constraint violationsqlx::query("INSERT INTO users VALUES ($1, $2)") .bind(id) .bind(email) .execute(&pool) .await .map_err(|e| BatchError::Database( format!("Insert failed: {}", e) ))?;When: Invalid configuration detected
Common Causes:
Example:
pub struct MyBuilder { source: Option<String>, target: Option<String>,}
impl MyBuilder { pub fn build(self) -> Result<MyReader, BatchError> { let source = self.source.ok_or_else(|| { BatchError::Configuration( "source path is required".to_string() ) })?;
let target = self.target.ok_or_else(|| { BatchError::Configuration( "target path is required".to_string() ) })?;
if source == target { return Err(BatchError::Configuration( "source and target cannot be the same".to_string() )); }
Ok(MyReader { source, target }) }}When: Data validation fails
Common Causes:
Example:
fn validate_record(record: &Record) -> Result<(), BatchError> { // Required field if record.email.is_empty() { return Err(BatchError::Validation( format!("Email is required for record {}", record.id) )); }
// Format validation if !record.email.contains('@') { return Err(BatchError::Validation( format!("Invalid email format: {}", record.email) )); }
// Range validation if record.age < 0 || record.age > 150 { return Err(BatchError::Validation( format!("Age {} is out of valid range (0-150)", record.age) )); }
// Business rule if record.amount > 10000.0 && !record.approved { return Err(BatchError::Validation( format!("Record {} exceeds limit without approval", record.id) )); }
Ok(())}When: Unrecoverable errors that should stop the job immediately
Common Causes:
Example:
impl ItemProcessor<Record, Record> for MyProcessor { fn process(&self, item: &Record) -> ItemProcessorResult<Record> { // Recoverable error (can skip) if item.age < 0 { return Err(BatchError::ItemProcessor( "Invalid age".to_string() )); }
// Fatal error (must stop) if self.config.is_corrupted() { return Err(BatchError::Fatal( "Configuration corrupted - cannot continue".to_string() )); }
// Security violation (fatal) if !self.verify_signature(&item.data) { return Err(BatchError::Fatal( "Signature verification failed - potential security breach".to_string() )); }
Ok(item.clone()) }}let step = StepBuilder::new("with-skips") .chunk::<Input, Output>(100) .reader(&reader) .processor(&processor) .writer(&writer) .skip_limit(10) // Skip up to 10 errors .build();When to Use:
let step = StepBuilder::new("strict") .chunk::<Input, Output>(100) .reader(&reader) .processor(&processor) .writer(&writer) // No skip_limit - any error stops the job .build();When to Use:
impl ItemReader<Record> for MyReader { fn read(&self) -> ItemReaderResult<Record> { let line = self.read_line() .map_err(|e| BatchError::ItemReader( format!("Failed to read line {} from {}: {}", self.line_number, self.file_path, e ) ))?;
self.parse_line(&line) .map_err(|e| BatchError::ItemReader( format!("Parse error at line {} in {}: {}", self.line_number, self.file_path, e ) )) }}use log::{error, warn};
impl ItemProcessor<Record, Record> for MyProcessor { fn process(&self, item: &Record) -> ItemProcessorResult<Record> { if let Err(e) = self.validate(item) { // Log error details error!("Validation failed for record {}: {}", item.id, e);
// Also include in BatchError return Err(BatchError::ItemProcessor( format!("Validation failed: {}", e) )); }
Ok(item.clone()) }}use std::thread;use std::time::Duration;
fn write_with_retry<T>( items: &[T], writer: &dyn ItemWriter<T>, max_retries: u32,) -> ItemWriterResult { let mut attempts = 0;
loop { match writer.write(items) { Ok(()) => return Ok(()), Err(e) if attempts < max_retries => { attempts += 1; warn!("Write failed (attempt {}): {}. Retrying...", attempts, e); thread::sleep(Duration::from_secs(2_u64.pow(attempts))); } Err(e) => { return Err(BatchError::ItemWriter( format!("Write failed after {} attempts: {}", attempts, e) )); } } }}use spring_batch_rs::error::BatchError;
fn handle_error(error: BatchError) { match error { BatchError::ItemReader(msg) => { eprintln!("Read error: {}", msg); // Maybe retry or skip } BatchError::ItemProcessor(msg) => { eprintln!("Process error: {}", msg); // Log to error file } BatchError::ItemWriter(msg) => { eprintln!("Write error: {}", msg); // Rollback transaction } BatchError::Fatal(msg) => { eprintln!("FATAL: {}", msg); std::process::exit(1); } _ => { eprintln!("Other error: {}", error); } }}let mut execution = StepExecution::new("my-step");step.execute(&mut execution)?;
// Check for errorsif execution.skip_count() > 0 { println!("Skipped {} items due to errors", execution.skip_count());}
if let Some(failure) = execution.failure_exceptions().first() { eprintln!("First failure: {}", failure);}
// Status checkmatch execution.status() { StepStatus::Completed => println!("Success!"), StepStatus::Failed => eprintln!("Step failed"), StepStatus::Stopped => println!("Step stopped"), _ => {}}#[derive(Debug)]pub enum MyAppError { InvalidFormat(String), RateLimitExceeded, ApiError(String),}
impl From<MyAppError> for BatchError { fn from(err: MyAppError) -> Self { match err { MyAppError::InvalidFormat(msg) => { BatchError::ItemProcessor(format!("Format error: {}", msg)) } MyAppError::RateLimitExceeded => { BatchError::ItemReader("API rate limit exceeded".to_string()) } MyAppError::ApiError(msg) => { BatchError::ItemReader(format!("API error: {}", msg)) } } }}
// Usagefn process_item(item: &Item) -> ItemProcessorResult<ProcessedItem> { let result = call_api(item)?; // MyAppError automatically converts Ok(result)}Descriptive Messages
Include item IDs, file names, line numbers in error messages
Context Propagation
Wrap lower-level errors with context about what you were doing
Appropriate Variants
Use the right BatchError variant for the situation
Logging
Log errors before returning them for debugging