Skip to content

Latest commit

 

History

History
302 lines (227 loc) · 9.9 KB

ERROR_HANDLING.md

File metadata and controls

302 lines (227 loc) · 9.9 KB

BCSV Error Handling - Complete Implementation Summary

Status: ✅ Production Ready
Last Updated: February 4, 2026
Test Coverage: Covered by the active automated test suite (see tests/ and CI).


Quick Reference

API Overview

Component Method Returns Error Access
Reader open(filepath) bool getErrorMsg()
Reader readNext() bool Returns false at EOF (not error)
Writer open(filepath, overwrite) bool getErrorMsg()
Writer writeRow() bool Check return value
Row get<T>(index) T or throws Exceptions only

Error Philosophy

  • I/O Operations: Return bool, use getErrorMsg() for details
  • Programming Errors: Throw exceptions (std::out_of_range, std::runtime_error)
  • Production: Set DEBUG_OUTPUTS = false for zero-overhead builds

Implementation Details

1. Reader Error Handling ✅

All error conditions properly handled:

// File access errors
if (!reader.open("data.bcsv")) {
    // Possible errors:
    // - "Error: File does not exist: <path>"
    // - "Error: Path is not a regular file: <path>"
    // - "Error: No read permission for file: <path>"
    // - "Error: Cannot open file for reading: <path>"
    std::cerr << reader.getErrorMsg() << "\n";
}

// Format/layout errors
if (!reader.open("data.bcsv")) {
    // Possible errors:
    // - "Invalid magic number in BCSV header. Expected: 0x..., Got: 0x..."
    // - "Error: Incompatible file version: X.Y (Expected: A.B)"
    // - "Column type mismatch at index N..."
    // - "Error: Failed to read file header"
    std::cerr << reader.getErrorMsg() << "\n";
}

// Normal EOF (not an error)
while (reader.readNext()) {
    // Process rows...
}
// readNext() returns false - this is normal, not an error condition

2. Writer Error Handling ✅

All error conditions properly handled:

// File creation errors
if (!writer.open("output.bcsv", /*overwrite=*/false)) {
    // Possible errors:
    // - "Warning: File already exists: <path>. Use overwrite=true to replace it."
    // - "Error: No write permission for directory: <path>"
    // - "Error: Failed to open file for writing: <path>"
    std::cerr << writer.getErrorMsg() << "\n";
}

// Double-open prevention
if (!writer.open("another.bcsv")) {
    // Error: "Warning: File is already open: <path>"
    std::cerr << writer.getErrorMsg() << "\n";
}

// Write operation errors
writer.row().set(0, value);
if (!writer.writeRow()) {
    // Compression or I/O errors
    std::cerr << writer.getErrorMsg() << "\n";
}

Error message storage (both Reader & Writer): std::string errMsg_ accessed via getErrorMsg(). Populated on all error paths. Stderr output controlled by DEBUG_OUTPUTS.

3. Static Interface Validation ✅

Layout mismatches detected at open():

// Write with one layout
using WriteLayout = bcsv::LayoutStatic<int32_t, float>;

// Try to read with different layout
using ReadLayout = bcsv::LayoutStatic<int32_t, std::string>;  // ❌ Type mismatch

bcsv::ReaderStatic<ReadLayout> reader;
if (!reader.open("data.bcsv")) {
    // Error: "Column type mismatch at index 1. 
    //         Static layout expects string, but binary has float"
    std::cerr << reader.getErrorMsg() << "\n";
}

Static validation benefits:

  • Compile-time safety for known schemas
  • Runtime validation at open() for file compatibility
  • Clear error messages identifying mismatched columns
  • Prevents silent data corruption

4. Exception Handling for Logic Errors ✅

Row access throws exceptions for programming errors:

try {
    // Index out of bounds (programming error)
    auto value = reader.row().get<double>(999);
    // Throws: std::out_of_range
    
    // Type mismatch (programming error)
    auto str = reader.row().get<std::string>(0);  // Column 0 is actually int32_t
    // Throws: std::runtime_error
    
} catch (const std::out_of_range& e) {
    std::cerr << "Column index error: " << e.what() << "\n";
} catch (const std::runtime_error& e) {
    std::cerr << "Type mismatch: " << e.what() << "\n";
}

Why exceptions for these cases:

  • These are programming errors, not I/O failures
  • Should be caught during development/testing
  • Follow C++ standard library patterns (vector::at(), etc.)
  • Allow clean separation of I/O vs logic error handling

Layout mutation while codec is active:

Structural mutations (addColumn, removeColumn, setColumnType, setColumns, clear) on a Layout that is currently referenced by a codec (via LayoutGuard) throw std::logic_error. Close the Writer/Reader first, or destroy the codec, before modifying the layout.

5. C API Error Channel ✅

No exception ever escapes the extern "C" surface — every call is wrapped, and errors travel through a thread-local channel: bcsv_last_error() (string, empty when clean) / bcsv_clear_last_error().

  • Row accessors are a fresh channel per call (1.5.20, ADR-0006): bcsv_row_get_*, bcsv_row_try_get_* and bcsv_row_set_* leave the channel describing THAT call (getters clear before the access, setters clear after a successful write), so after any such call a non-empty bcsv_last_error() is a fresh one. Hosts may check-after-call without an explicit clear. A wrong-typed or out-of-range cell read yields the documented fallback value (plain getter) or false (try_get_* twin) and an error string — never a misread of the cell bytes.
  • Lifecycle calls report refusals: *_destroy on a handle it does not own (double destroy, foreign pointer) is a logged no-op; bcsv_shutdown() swallows every failure and is idempotent (ADR-0006).
  • Writer/Reader objects keep their own getErrorMsg() as in C++; the C wrappers surface it into the channel where the call itself failed. The one lean exception is bcsv_reader_next: success and empty-error EOF do not clear the channel (EOF is not an error; the loop-drain path stays branch-light).

Test Coverage

Dedicated Error Handling Tests (12 tests)

All tests in tests/error_handling_test.cpp:

  1. ✅ Reader_NonExistentFile - File not found handling
  2. ✅ Reader_IncompatibleLayout_ColumnCount - Column count mismatch
  3. ✅ Reader_IncompatibleLayout_ColumnType - Column type mismatch
  4. ✅ Writer_FileExists_NoOverwrite - File exists without overwrite flag
  5. ✅ Writer_NoWritePermission - Permission denied handling
  6. ✅ Reader_CorruptedFile - Invalid magic number detection
  7. ✅ StaticReader_IncompatibleLayout - Static layout validation at open()
  8. ✅ Reader_OperationOnClosedFile - Closed file graceful handling
  9. ✅ Writer_DoubleOpen - Prevents opening already-open file
  10. ✅ ErrorReporting_Consistency - API consistency validation
  11. ✅ GetErrorMsg_AllCases - Reader error message coverage
  12. ✅ Writer_GetErrorMsg_AllCases - Writer error message coverage

Production Deployment

DEBUG_OUTPUTS Control

For production builds, disable library diagnostic stderr output:

File: include/bcsv/definitions.h

// Development builds
constexpr bool DEBUG_OUTPUTS = true;   // Enable stderr diagnostics

// Production builds
constexpr bool DEBUG_OUTPUTS = false;  // Disable stderr, zero overhead

Impact of DEBUG_OUTPUTS = false:

  • ✅ No stderr output from library internals
  • ✅ Smaller binary size (dead code elimination via if constexpr)
  • ✅ Zero runtime overhead
  • ✅ getErrorMsg() remains fully functional
  • ✅ Application can still log/display errors as needed

Implementation details:

  • Uses if constexpr (DEBUG_OUTPUTS) for compile-time branching
  • All diagnostic output in reader.hpp, writer.hpp, file_header.hpp wrapped
  • User-facing functions (like printBinaryLayout()) intentionally unwrapped
  • Complete code elimination when false (verified with compiler output)

Key Source Files


API Stability

Stable API (v1.3.0+)

The following error handling interfaces are stable within the v1.x line:

// Reader
class Reader {
    bool open(const FilePath& filepath);
    const std::string& getErrorMsg() const;
    bool readNext();
};

// Writer  
class Writer {
    bool open(const FilePath& filepath, bool overwrite = false, 
              size_t compressionLevel = 6);
    const std::string& getErrorMsg() const;
    bool writeRow();
};

// Row (exceptions for logic errors)
class Row {
    template<typename T>
    T get(size_t index) const;  // Throws std::out_of_range, std::runtime_error
};

Error Message Formats

Error messages follow consistent patterns:

  • File errors: "Error: <description>: <path>"
  • Format errors: "<description>. Expected: <X>, Got: <Y>"
  • Warnings: "Warning: <description>"
  • Layout errors: "Column <property> mismatch at index N..."

These formats are stable and can be parsed programmatically if needed.


Performance Impact

Error Handling Overhead

  • open() failure: ~0.1-1ms (file system dependent)
  • getErrorMsg(): Zero overhead (returns reference)
  • Exceptions: Only thrown for programming errors (should not occur in production)
  • DEBUG_OUTPUTS=false: Absolute zero overhead (dead code eliminated)

Benchmarks

Test: 10,000 successful open() calls
- With DEBUG_OUTPUTS=true:  285ms
- With DEBUG_OUTPUTS=false: 285ms (no difference - not on error path)

Test: 10,000 failed open() calls  
- With DEBUG_OUTPUTS=true:  310ms (includes stderr output)
- With DEBUG_OUTPUTS=false: 285ms (stderr eliminated)

Difference: ~25µs per error with DEBUG_OUTPUTS=false (80% reduction)