Answer to the question.
History of the question
Error handling in C++ traditionally relied on exceptions or error codes. Exceptions provided clean syntax but incurred runtime overhead and were difficult to use in deterministic contexts like embedded systems or real-time trading. Error codes were efficient but polluted function signatures and required manual propagation checking. C++23 introduced std::expected, a vocabulary type representing either a value or an error, inspired by functional programming monads like Haskell's Either or Rust's Result.
The problem
While std::expected provides monadic operations like and_then, or_else, and transform, these operations require explicit handling of the error type at each step of the composition chain. Unlike exception-based handling where errors propagate automatically up the call stack until caught, std::expected requires the programmer to explicitly specify how errors transform or propagate through each monadic bind. This explicitness creates verbose code when chaining multiple operations that might fail, and requires careful consideration of error type conversions when different operations return different error types. The fundamental issue is that C++'s type system requires explicit error type unification in template instantiations, unlike dynamic exception handling.
The solution
C++23's std::expected monadic interface uses explicit template machinery to ensure type safety and zero-overhead abstraction. The and_then method requires the callable to return another std::expected with potentially different error types, and the implementation uses SFINAE or concepts to validate the composition. For error type propagation, developers must explicitly handle type conversions using or_else or map error types using transform_error. This explicit approach ensures that error handling paths are visible in the source code and optimizable by the compiler, unlike hidden exception control flow. The solution embraces functional programming principles while respecting C++'s zero-overhead philosophy.
#include <expected> #include <string> #include <system_error> std::expected<int, std::error_code> parse_int(const std::string& s); std::expected<double, std::error_code> divide(int a, int b); // Explicit error handling in composition auto result = parse_int("42") .and_then([](int n) { return divide(100, n); }) .or_else([](std::error_code e) { return std::expected<double, std::error_code>(0.0); });
Situation from life
A medical device software team needed to implement a data pipeline processing sensor readings with multiple validation stages. Each stage could fail with specific error codes (hardware timeout, checksum failure, calibration error) that needed propagation to the logging system with full type safety.
The first approach considered was exception-based error handling using std::runtime_error hierarchies. This allowed automatic propagation up the call stack and clean separation of error handling from business logic. However, medical devices required deterministic latency guarantees, and exceptions introduced unpredictable overhead during stack unwinding. The approach also made it impossible to use the code in GPU kernels or embedded contexts where exceptions were disabled. The team needed a solution that worked in noexcept environments.
The second approach considered was traditional error codes using std::optional or std::variant with manual error checking after each operation. This provided the required determinism and noexcept compatibility. However, the code became cluttered with repetitive if (!result) checks after every pipeline stage. Error propagation required manual threading of error codes through the call stack, and composing multiple operations required nested conditionals that obscured the data flow logic. The error types also lacked type safety when mixing different error categories from various hardware sensors.
The chosen solution was C++23's std::expected with its monadic interface. The team refactored the pipeline to use and_then for chaining validation steps and or_else for error transformation. This preserved the linear data flow while maintaining explicit error handling paths. The solution provided zero-overhead abstraction compatible with noexcept constraints and allowed precise error type propagation to the logging system. The refactoring took three weeks, after which the codebase supported 15 different sensor types with unified error handling.
What candidates often miss
How does std::expected handle type erasure when chaining operations that return different error types?
Candidates often miss that std::expected does not perform type erasure by default. When using and_then, the callable must return a std::expected with the same error type as the original, or the program fails to compile.
To handle different error types, developers must explicitly transform errors using transform_error or use std::expected with a common error type variant. Unlike exceptions which use a single static type for all errors (usually std::exception_ptr or base exception classes), std::expected maintains strict type safety.
This design prevents hidden type erasure costs but requires explicit error type unification at compile time. Understanding this distinction is crucial for composing operations from different libraries with distinct error categories.
Why does std::expected not provide a monadic bind operation that automatically propagates errors like exception handling does?
Candidates frequently confuse std::expected with exception-based error handling regarding automatic propagation. They expect that if an operation in a chain fails, subsequent operations will be skipped automatically without explicit handling.
While and_then does skip the callable on error, the error type must still be explicitly handled at the end of the chain or transformed using or_else. The fundamental reason is that C++'s type system requires explicit handling of all possible error states to maintain zero-overhead and deterministic behavior.
Automatic propagation would require implicit control flow similar to exceptions, which contradicts the design goal of explicit, optimizable error paths. Std::expected prioritizes performance and determinism over syntactic convenience.
How does the noexcept specification of std::expected monadic operations affect exception safety guarantees in composition chains?
Candidates often miss that std::expected monadic operations like and_then and transform are conditionally noexcept based on the operations they invoke. If the callable passed to and_then is noexcept, the entire chain remains noexcept.
However, if the callable might throw, the operation may throw std::bad_expected_access or propagate the exception depending on the specific implementation and error handling strategy. This conditional noexcept propagation allows developers to maintain strong exception safety guarantees throughout the composition chain.
Understanding this is crucial for real-time systems where exception specifications affect code generation and optimization. The noexcept contract propagates through the monadic chain, ensuring that error handling remains deterministic and optimizable by the compiler.