Aller au contenu

Error Types Reference

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:

  • File not found
  • Invalid file format
  • Database connection issues
  • Network timeouts
  • Parse errors

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:

  • Include context (file name, line number, query)
  • Wrap underlying errors with context
  • Use descriptive messages

When: Item transformation or validation fails

Common Causes:

  • Validation failures
  • Type conversion errors
  • Business rule violations
  • External API errors

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:

  • Disk full
  • Permission denied
  • Database constraints violated
  • Network failures
  • Transaction failures

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:

  • Include item identifiers in errors
  • Distinguish between recoverable and fatal errors
  • Ensure cleanup in close() even on errors

When: Single-task operations fail

Common Causes:

  • File operations fail
  • External command errors
  • Network operations fail
  • Resource unavailable

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:

  • File not found
  • Permission denied
  • Disk full
  • Invalid path

Example:

use std::fs::File;
// Automatic conversion
let file = File::open("data.csv")?; // io::Error -> BatchError::Io
// Manual conversion
let file = File::open("data.csv")
.map_err(|e| BatchError::Io(e))?;
// With context
let file = File::open(&path)
.map_err(|e| BatchError::ItemReader(
format!("Cannot open {}: {}", path, e)
))?;

When: Database operations fail

Common Causes:

  • Connection failures
  • Query syntax errors
  • Constraint violations
  • Deadlocks
  • Timeout

Example:

// Query error
sqlx::query("SELECT * FROM users")
.fetch_all(&pool)
.await
.map_err(|e| BatchError::Database(
format!("Query failed: {}", e)
))?;
// Connection error
PgPool::connect(&url)
.await
.map_err(|e| BatchError::Database(
format!("Cannot connect to database: {}", e)
))?;
// Constraint violation
sqlx::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:

  • Missing required parameters
  • Invalid parameter values
  • Conflicting settings
  • Builder validation failures

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:

  • Invalid data format
  • Out-of-range values
  • Missing required fields
  • Failed business rules

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:

  • Critical system failures
  • Corrupted data structures
  • Security violations
  • Unrecoverable state

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:

  • Data quality issues are expected
  • Individual failures shouldn’t stop the job
  • You log skipped items separately
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:

  • Data must be perfect
  • Any error indicates a serious problem
  • Rollback and manual intervention required
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 errors
if 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 check
match 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))
}
}
}
}
// Usage
fn 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