Status: ✅ Production Ready
Last Updated: February 4, 2026
Test Coverage: Covered by the active automated test suite (see tests/ and CI).
| 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 |
- I/O Operations: Return
bool, usegetErrorMsg()for details - Programming Errors: Throw exceptions (
std::out_of_range,std::runtime_error) - Production: Set
DEBUG_OUTPUTS = falsefor zero-overhead builds
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 conditionAll 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 viagetErrorMsg(). Populated on all error paths. Stderr output controlled byDEBUG_OUTPUTS.
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
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.
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_*andbcsv_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-emptybcsv_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) orfalse(try_get_*twin) and an error string — never a misread of the cell bytes. - Lifecycle calls report refusals:
*_destroyon 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 isbcsv_reader_next: success and empty-error EOF do not clear the channel (EOF is not an error; the loop-drain path stays branch-light).
All tests in tests/error_handling_test.cpp:
- ✅ Reader_NonExistentFile - File not found handling
- ✅ Reader_IncompatibleLayout_ColumnCount - Column count mismatch
- ✅ Reader_IncompatibleLayout_ColumnType - Column type mismatch
- ✅ Writer_FileExists_NoOverwrite - File exists without overwrite flag
- ✅ Writer_NoWritePermission - Permission denied handling
- ✅ Reader_CorruptedFile - Invalid magic number detection
- ✅ StaticReader_IncompatibleLayout - Static layout validation at open()
- ✅ Reader_OperationOnClosedFile - Closed file graceful handling
- ✅ Writer_DoubleOpen - Prevents opening already-open file
- ✅ ErrorReporting_Consistency - API consistency validation
- ✅ GetErrorMsg_AllCases - Reader error message coverage
- ✅ Writer_GetErrorMsg_AllCases - Writer error message coverage
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 overheadImpact 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)
- tests/error_handling_test.cpp — dedicated error handling test suite
- include/bcsv/reader.hpp / writer.hpp — implementation
- include/bcsv/definitions.h —
DEBUG_OUTPUTSconstant
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 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.
- 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)
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)