Skip to content

Error Types Reference

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