Mastering Error Handling with C++23 std::expected
Why I Switched from Exceptions to std::expected
When I first started using C++23, I was surprised how quickly I grew accustomed to std::expected. For years I relied on exceptions for error propagation and manual error codes for performance‑critical paths. Both approaches have merit, but they each carry drawbacks: exceptions can obscure control flow, while error codes force callers to check a series of if statements. In a project that deals with network I/O and file parsing, I needed a way to express that a function either returns a value or a descriptive error without sacrificing type safety. std::expected gave me exactly that.
A Real‑World Scenario: Parsing Configuration Files
Our service reads a JSON configuration at startup. The parsing routine can fail for many reasons—malformed JSON, missing required fields, or permission errors. The old code looked like this:
bool ParseConfig(const std::string& data, Config& out) {
// ... parse logic ...
if (parse_failed) {
std::cerr << "Parse error at line " << line_num;
return false;
}
out = parsed;
return true;
}
Every caller had to remember to check the return value, and the error information was lost after the first error. Switching to std::expected<Config, std::string> let us express the intent directly:
std::expected<Config, std::string> ParseConfig(const std::string& data) {
// ... parse logic ...
if (parse_failed) {
return std::unexpected("Parse error at line " + std::to_string(line_num));
}
return parsed;
}
Now the caller can write auto cfg = ParseConfig(data) and either use cfg.value() or handle the error via cfg.error(). The type system guarantees that we cannot accidentally ignore the error.
What std::expected Actually Is
std::expected is a template that holds either a value of type Because the error type is part of the template parameters, the compiler can warn us about mismatched handling. This is a huge win over raw One of the most compelling features is the ability to chain operations. If we have a function that reads a file and returns Using Notice how the error handling is explicit and does not involve stack unwinding. The composition stays in the same thread, which is crucial for low‑latency services.p>
At first I worried about the overhead of an extra pointer or small‑object allocation. In practice, Even in those scenarios, I still keep exceptions for truly unexpected failures—things like out‑of‑memory or assertion violations. By following these guidelines, you get the clarity of algebraic data types without the boilerplate of hand‑rolled result types.p>
Integrating T or an error of type E. It mirrors std::optional> but adds an explicit error type. The interface includes:p>
std::expected<T, E> obj] value initializationli>
bool has_value() const noexceptcode> – tells us if we have a valueli>
T& value() const] – throws if errorli>
E& error() const] – throws if valueli>
bool> via operator bool() const noexceptcode>li>
ul>
std::string> error messages.p>
Composing Expected Valuesh2>
std::expected<std::string, std::string>de>, we can pipe it through a parser:p>
and_thencode> and or_elsecode> lets us write flat, readable error‑propagation chains that look like synchronous code.n>p>
blockquote>
auto read_file(const std::string& path) {
// returns std::expected<std::string, std::string>
}
auto parse_json(const std::string& text) {
// returns std::expected<Config, std::string>
}
auto config = read_file(path)
.and_then(parse_json)
.or_else([](const std::string& err) {
std::cerr << "Failed to load config: " << err;
return std::expected<Config, std::string>{std::unexpect, "fallback"};
});code>pre>
Performance Considerationsh2>
std::expectedde> is essentially a When to Prefer std::expected Over Exceptionsn>
std::expectedde> shines when the error is part of the normal operation domain.p>
Best Practicesn>
std::string>, std::error_code>, or a custom enum wrapped in a struct.n>
and_thencode> and or_else> for linear error handling; avoid deep nesting.n>
std::expectedde> with raw nullptr> checks; the type system should enforce consistency.n>
std::expectedde> is Conclusionn>
std::expectedde> into our configuration‑parsing pipeline was a small change that dramatically improved code readability and reliability. The type system now guarantees that errors are either handled or propagated, and the composition operators let us write error‑aware code that looks almost like synchronous code. If you are working on any C++23 project, I highly recommend giving std::expectedde> a try—you’ll likely find it becomes an essential part of your modern error‑handling toolkit.p>