Skip to content

Adopt std::expected (C++23) for error handling instead of (or in addition to) exceptions #57

Description

@rgr-proton

Description

Current Situation

The C++ binding defines a custom tidesdb::Exception (derived from std::runtime_error) and throws it on most error paths (failed opens, I/O errors, conflicts, invalid arguments, etc.). While this is convenient and idiomatic for many C++ applications, it introduces noticeable overhead in performance-critical code:

  • Stack unwinding cost (even with -fno-exceptions disabled in hot paths)
  • Difficulty in writing zero-overhead, no-throw hot loops (common in trading systems, time-series ingestion, ML feature stores, etc.)
  • Less explicit control flow — callers must either use try/catch everywhere or let exceptions propagate

The library already maintains a rich ErrorCode enum, which makes it a perfect candidate for modern value-based error handling.

Proposed Improvement

Introduce std::expected<T, ErrorCode> (or a small wrapper Expected<T>) as the primary return type for operations that can fail, while optionally keeping the exception-based API for backward compatibility.

Benefits:

  • Zero runtime cost on the success path (no exceptions, no unwinding)
  • Explicit, forced error handling at the call site → fewer swallowed errors
  • Better performance in tight loops and hot paths (critical for your use case and many other high-throughput workloads)
  • Composable with modern C++ (monadic operations like .and_then(), .or_else(), | in C++23)
  • Cleaner integration with std::span changes
  • Aligns with trends in high-performance libraries

Backward Compatibility
Provide both APIs:

  • Keep existing exception-throwing methods (perhaps under a different name or via a template policy)
  • Or add _expected / _noexcept suffixed versions that return std::expected

Concrete Suggestions

  1. Helper type (at the top of the header):

    template<typename T = void>
    using Expected = std::expected<T, ErrorCode>;
  2. Example API changes (add alongside existing methods):

    // TidesDB
    [[nodiscard]] Expected<void> createColumnFamily(const std::string& name,
                                                    const ColumnFamilyConfig& config);
    
    [[nodiscard]] Expected<ColumnFamily> getColumnFamily(const std::string& name);
    
    void commit();                    // existing (throws)
    [[nodiscard]] Expected<void> commit_expected();  // new
    
    // Iterator, ColumnFamily methods, etc. similarly
  3. Error conversion utilities:

    [[nodiscard]] static Expected<void> fromCError(int c_err);  // maps tidesdb error codes
  4. C++ Standard

Lifetime / Design Notes

  • Success path returns the value directly.
  • Failure path carries the precise ErrorCode (already beautifully defined).
  • For operations that currently return std::vector<std::uint8_t> on "not found", we can decide between Expected<std::vector<...>> (empty on not-found) or using ErrorCode::NotFound explicitly.

Motivation / Use Case

In low-latency / high-frequency applications, exception-based error handling is often deliberately avoided. Value-based errors with std::expected provide the best of both worlds: safety and performance.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions