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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions