Skip to content

Exception Handling and Exit Codes

This document describes BatteryProc's exception hierarchy and how exceptions map to CLI exit codes.

Overview

BatteryProc uses a structured exception hierarchy to categorize different types of processing failures. This enables: - Clean error messages at the CLI level - Deterministic exit codes for scripting and automation - Programmatic error handling via the public API

Exception Hierarchy

All domain-specific exceptions inherit from BatteryAnalysisError:

BatteryAnalysisError (base class)
├── ParserError           # File parsing failures
├── BadCellError          # Data quality issues
└── WrongDataTypeError    # Type/format mismatches

BatteryAnalysisError

Location: batteryproc.errors.BatteryAnalysisError

Base exception for all BatteryProc domain errors. Use this when: - You need to catch any BatteryProc-specific error - You're implementing generic error handling

Example:

from batteryproc.errors import BatteryAnalysisError

try:
    result = process_battery_data(file_path)
except BatteryAnalysisError as e:
    logger.error(f"BatteryProc error: {e}")

ParserError

Location: batteryproc.errors.ParserError

Raised when file parsing fails. Common scenarios: - Missing required sheets or columns in Excel files - Invalid file format or structure - Corrupt or unreadable data - Unsupported file types

CLI Behavior: Exit code 2 (FileErrorCategory.PARSE)

Example:

from batteryproc.errors import ParserError

def parse_neware_file(path):
    if not path.exists():
        raise ParserError(f"File not found: {path}")

    # Check for required sheets
    if 'record' not in sheets:
        raise ParserError("Missing required sheet: 'record'")

BadCellError

Location: batteryproc.errors.BadCellError

Raised when battery test data fails quality checks. Common scenarios: - Insufficient charge/discharge capacity - Missing or invalid cycle data - Anomalous electrochemical behavior - Data quality below processing thresholds

CLI Behavior: Exit code 3 (FileErrorCategory.DATA_QUALITY)

Example:

from batteryproc.errors import BadCellError

def check_capacity(capacity_df):
    max_discharge = capacity_df['discharge_mAh_per_g'].max()
    if max_discharge < 10:
        raise BadCellError(
            f"Discharge capacity ({max_discharge:.1f} mAh/g) "
            f"below 10 mAh/g threshold"
        )

WrongDataTypeError

Location: batteryproc.errors.WrongDataTypeError

Raised when data types or formats don't match expectations. Common scenarios: - Expected DataFrame but received dict - Required numeric column contains strings - Data structure incompatible with operation - Missing expected attributes or methods

CLI Behavior: Exit code 4 (FileErrorCategory.WRONG_DATA_TYPE)

Example:

import pandas as pd

from batteryproc.errors import WrongDataTypeError

def process_cycle_data(data):
    if not isinstance(data, pd.DataFrame):
        raise WrongDataTypeError(
            f"Expected DataFrame, got {type(data).__name__}"
        )

CLI Exit Code Mapping

The CLI maps exceptions to specific exit codes for automation and scripting:

Exit Code Category Exception Type Description
0 Success — All files processed successfully
1 Unexpected Generic Exception Unexpected/unhandled errors
2 Parse ParserError File parsing failures
3 Data Quality BadCellError Battery data quality issues
4 Wrong Data Type WrongDataTypeError Type/format mismatches
13 Permission PermissionError File access denied

Multi-File Processing

When processing multiple files, the CLI follows these rules:

  1. At least one success: Exit with code 0 if any file succeeds
  2. All failures: Exit with the most severe error code encountered
  3. Error priority: PERMISSION > PARSE > DATA_QUALITY > WRONG_DATA_TYPE > UNEXPECTED

This means if you process 3 files and get: - File 1: Success - File 2: BadCellError (exit code 3) - File 3: ParserError (exit code 2)

The CLI will exit with code 0 (at least one success).

However, if all three fail: - File 1: BadCellError (exit code 3) - File 2: ParserError (exit code 2) - File 3: WrongDataTypeError (exit code 4)

The CLI will exit with code 2 (PARSE has higher priority than DATA_QUALITY and WRONG_DATA_TYPE).

Programmatic API

The batteryproc.api module provides exception-free processing via structured results:

from pathlib import Path
from batteryproc.api import process_file
from batteryproc.batteryproc_types import ProcessOptions

# Process a file
result = process_file(Path("data.xlsx"), ProcessOptions())

# Check result
if result.ok:
    print(f"Success! Processed {result.path.name}")
    print(f"Capacity data: {result.cell.capacity}")
else:
    print(f"Failed: {result.status}")
    print(f"Error category: {result.error_category}")
    print(f"Errors: {result.errors}")

The API never raises sys.exit(). Instead, it returns FileResult objects with: - ok: Boolean success/failure flag - status: "success", "failure", or "skipped" - error_category: One of FileErrorCategory enum values - errors: List of error messages - cell: Battery data (if successful)

Implementation Details

Exception Flow

  1. Raise in domain code:

    # In batteryproc/parsers.py
    if required_sheet not in wb.sheetnames:
        raise ParserError(f"Missing sheet: {required_sheet}")
    

  2. Catch in API layer:

    # In batteryproc/api.py (_process_single)
    try:
        outcome = processing.process_file_pipeline(normalized_path, options)
    except ParserError as exc:
        return _make_error_result(
            normalized_path,
            f"Parse error for {normalized_path.name}: {exc}",
            "failure",
            ...,
            error_category=FileErrorCategory.PARSE,
        )
    

The pipeline entry point processing.process_file_pipeline composes processing.parse_file → processing.analyze_cell → processing.write_artifacts. Domain errors surface from the parse stage (parse_file raises ParserError, WrongDataTypeError, or PermissionError); _process_single translates each raised exception to the matching FileErrorCategory. Non-raising conditions such as a blocked bad cell are reported through the returned PipelineOutcome (mapped onto FileResult) rather than as exceptions.

  1. Map to exit code in CLI:
    # In batteryproc/cli.py
    _FILE_ERROR_EXIT_CODES = {
        FileErrorCategory.PARSE: 2,
        FileErrorCategory.DATA_QUALITY: 3,
        # ...
    }
    
    exit_code = _FILE_ERROR_EXIT_CODES.get(
        result.error_category, 
        _DEFAULT_ERROR_EXIT_CODE
    )
    sys.exit(exit_code)
    

When to Use Each Exception

Use ParserError when: - Reading/parsing the input file fails - File format is invalid or unsupported - Required data structures are missing

Use BadCellError when: - Battery data fails quality checks - Capacity is too low for analysis - Cycle data is incomplete or invalid - --force-process can override these checks - batteryproc.formulas.normalize_inputs now surfaces data-quality issues (missing columns, empty results, trimmed rows) as BadCellError so CLI/API exits with code 3. - _prepare_normalized_inputs in batteryproc.metrics raises BadCellError when normalization trims rows and legacy fallbacks are disabled.

Use WrongDataTypeError when: - Function receives wrong data type - Data structure doesn't match expectations - Usually indicates a programming error - _prepare_normalized_inputs raises WrongDataTypeError for invalid mass/weight overrides supplied by users or config files. - configure_cycle_detection validates numeric tuning parameters and raises WrongDataTypeError for non-numeric or out-of-range values, mapping these configuration issues to exit code 4.

Use generic exceptions when: - Error is truly unexpected - No specific domain category applies - You want a stack trace for debugging

Testing

Exception handling is tested in tests/test_api.py:

def test_process_file_error_classification(mocker):
    """Verify exceptions map to correct error categories."""

    # Test ParserError → PARSE
    mocker.patch(
        "batteryproc.processing.process_file_pipeline",
        side_effect=ParserError("test"),
    )
    result = api.process_file(path, options)
    assert result.error_category == FileErrorCategory.PARSE

    # Test BadCellError → DATA_QUALITY
    mocker.patch(
        "batteryproc.processing.process_file_pipeline",
        side_effect=BadCellError("test"),
    )
    result = api.process_file(path, options)
    assert result.error_category == FileErrorCategory.DATA_QUALITY

Patch batteryproc.processing.process_file_pipeline (the live per-file entry point _process_single calls) — or a lower-level target such as batteryproc.processing.parse_file — rather than any cli helper.

References

  • Module: batteryproc/errors.py - Exception definitions
  • API: batteryproc/api.py - Exception handling
  • CLI: batteryproc/cli.py - Exit code mapping
  • Types: batteryproc/batteryproc_types.py - FileErrorCategory enum
  • Tests: tests/test_api.py - Exception handling tests