Search Knowledge

© 2026 LIBREUNI PROJECT

Modern C++ Programming / Modern C++20/23

std::expected (C++23)

Error Handling with std::expected

C++23 introduces std::expected<T, E>, a vocabulary type for functions that can return either a value or an error. It provides a type-safe alternative to exceptions and error codes.

Basic Concept

std::expected<T, E> holds either a value of type T (success) or an error of type E (failure):

#include <expected>
#include <string>

std::expected<int, std::string> parse_int(const std::string& str) {
    try {
        return std::stoi(str);
    } catch (...) {
        return std::unexpected("Invalid integer format");
    }
}

auto result = parse_int("42");
if (result) {
    std::cout << "Parsed: " << *result << '\n';  // 42
} else {
    std::cout << "Error: " << result.error() << '\n';
}

Checking Success vs Failure

std::expected<double, std::string> divide(double a, double b) {
    if (b == 0.0) {
        return std::unexpected("Division by zero");
    }
    return a / b;
}

auto result = divide(10.0, 2.0);

// Method 1: Contextual conversion to bool
if (result) {
    double value = *result;  // Dereference to get value
}

// Method 2: has_value()
if (result.has_value()) {
    double value = result.value();  // Throws if error
}

// Method 3: Check for error
if (!result) {
    std::string err = result.error();
}

Accessing Values

auto result = divide(10.0, 2.0);

// Dereference (undefined behavior if error)
double v1 = *result;
double v2 = result.operator*();

// value() - throws std::bad_expected_access if error
double v3 = result.value();

// value_or() - returns default if error
double v4 = result.value_or(0.0);

Error Types

Using Enums

enum class Error {
    FileNotFound,
    PermissionDenied,
    InvalidFormat
};

std::expected<std::string, Error> read_file(const std::string& path) {
    if (!file_exists(path)) {
        return std::unexpected(Error::FileNotFound);
    }
    if (!has_permission(path)) {
        return std::unexpected(Error::PermissionDenied);
    }
    // Read and return content
    return file_content;
}

Custom Error Types

struct ParseError {
    std::string message;
    size_t position;
    
    ParseError(std::string msg, size_t pos) 
        : message(std::move(msg)), position(pos) {}
};

std::expected<int, ParseError> parse_number(const std::string& str) {
    for (size_t i = 0; i < str.length(); ++i) {
        if (!std::isdigit(str[i])) {
            return std::unexpected(ParseError{
                "Invalid character", i
            });
        }
    }
    return std::stoi(str);
}

Monadic Operations

C++23 expected supports monadic operations for chaining:

and_then: Chain Operations

std::expected<int, std::string> parse_int(const std::string& s);
std::expected<int, std::string> validate_positive(int n);
std::expected<double, std::string> compute_sqrt(int n);

auto result = parse_int("16")
    .and_then(validate_positive)
    .and_then(compute_sqrt);

if (result) {
    std::cout << "Square root: " << *result << '\n';  // 4.0
}

Implementation:

std::expected<int, std::string> validate_positive(int n) {
    if (n > 0) {
        return n;
    }
    return std::unexpected("Number must be positive");
}

std::expected<double, std::string> compute_sqrt(int n) {
    return std::sqrt(n);
}

or_else: Handle Errors

auto result = parse_int("invalid")
    .or_else([](const std::string& err) -> std::expected<int, std::string> {
        std::cerr << "Parse error: " << err << '\n';
        return 0;  // Return default value
    });

transform: Map Values

auto result = parse_int("42")
    .transform([](int n) { return n * 2; });  // std::expected<int, std::string>

if (result) {
    std::cout << *result << '\n';  // 84
}

transform_error: Map Errors

auto result = parse_int("invalid")
    .transform_error([](const std::string& err) {
        return Error::InvalidFormat;  // Convert error type
    });
// result is std::expected<int, Error>

Practical Example: File Processing Pipeline

enum class FileError {
    NotFound,
    PermissionDenied,
    InvalidFormat,
    ProcessingFailed
};

std::expected<std::string, FileError> read_file(const std::string& path);
std::expected<Data, FileError> parse_data(const std::string& content);
std::expected<Result, FileError> process_data(const Data& data);
std::expected<void, FileError> save_result(const Result& result);

// Chain operations
auto outcome = read_file("input.txt")
    .and_then(parse_data)
    .and_then(process_data)
    .and_then([](const Result& r) {
        return save_result(r);
    });

if (!outcome) {
    switch (outcome.error()) {
        case FileError::NotFound:
            std::cerr << "File not found\n";
            break;
        case FileError::PermissionDenied:
            std::cerr << "Permission denied\n";
            break;
        // ...
    }
}

std::expected<void, E>

For operations that don’t return a value:

std::expected<void, std::string> validate_config(const Config& cfg) {
    if (cfg.port < 1024) {
        return std::unexpected("Port must be >= 1024");
    }
    if (cfg.host.empty()) {
        return std::unexpected("Host cannot be empty");
    }
    return {};  // Success
}

auto result = validate_config(config);
if (!result) {
    std::cerr << "Validation failed: " << result.error() << '\n';
}

Comparison with Alternatives

vs Exceptions

// Exceptions: implicit control flow
double divide_throws(double a, double b) {
    if (b == 0.0) throw std::runtime_error("Division by zero");
    return a / b;
}

try {
    auto result = divide_throws(10.0, 0.0);
} catch (const std::exception& e) {
    // Handle error
}

// std::expected: explicit error handling
std::expected<double, std::string> divide_expected(double a, double b) {
    if (b == 0.0) return std::unexpected("Division by zero");
    return a / b;
}

auto result = divide_expected(10.0, 0.0);
if (!result) {
    // Handle error explicitly
}

vs std::optional

// std::optional: success/failure, but no error information
std::optional<int> parse_int_opt(const std::string& s) {
    try {
        return std::stoi(s);
    } catch (...) {
        return std::nullopt;  // No error information
    }
}

// std::expected: success/failure with error details
std::expected<int, std::string> parse_int_exp(const std::string& s) {
    try {
        return std::stoi(s);
    } catch (const std::exception& e) {
        return std::unexpected(e.what());  // Error details preserved
    }
}

vs Error Codes

// C-style error codes
int parse_int_code(const std::string& s, int* out, std::string* error) {
    try {
        *out = std::stoi(s);
        return 0;  // Success
    } catch (const std::exception& e) {
        *error = e.what();
        return -1;  // Failure
    }
}

// std::expected: type-safe, ergonomic
std::expected<int, std::string> parse_int(const std::string& s) {
    try {
        return std::stoi(s);
    } catch (const std::exception& e) {
        return std::unexpected(e.what());
    }
}

Error Propagation

Simplify error propagation with monadic operations:

std::expected<int, Error> step1();
std::expected<double, Error> step2(int);
std::expected<std::string, Error> step3(double);

std::expected<std::string, Error> process() {
    return step1()
        .and_then(step2)
        .and_then(step3);
    // Error automatically propagates through chain
}

Best Practices

  1. Use for recoverable errors: std::expected is ideal for expected failures (validation, parsing, I/O)
  2. Use exceptions for exceptional cases: Programming errors, violations of invariants
  3. Consistent error types: Use enums or custom error types across a module
  4. Leverage monadic operations: Chain operations for clean error handling
  5. Document error cases: Clearly document what errors a function can return

When to Use std::expected

✅ Use when:

  • Errors are expected and recoverable
  • Performance is critical (no exception overhead)
  • Error information needs to be propagated
  • Explicit error handling is preferred

❌ Don’t use when:

  • Errors are truly exceptional (use exceptions)
  • Error handling is optional (use std::optional)
  • Working with legacy code that uses error codes
Conceptual Check

What does std::expected<T, E> contain?

Interactive Lab

Complete the Code

std::expected<double, std::string> safe_sqrt(double x) {
    if (x < 0) {
        return std::("Cannot compute sqrt of negative number");
    }
    return std::sqrt(x);
}
Runtime Environment

Interactive Lab

1#include <iostream>
2#include <expected>
3#include <string>
4#include <cmath>
5 
6std::expected<double, std::string> safe_divide(double a, double b) {
7 if (b == 0.0) {
8 return std::unexpected("Division by zero");
9 }
10 return a / b;
11}
12 
13std::expected<double, std::string> safe_sqrt(double x) {
14 if (x < 0) {
15 return std::unexpected("Cannot compute sqrt of negative");
16 }
17 return std::sqrt(x);
18}
19 
20int main() {
21 auto result = safe_divide(16.0, 4.0)
22 .and_then(safe_sqrt);
23
24 if (result) {
25 std::cout << "Result: " << *result << '\n';
26 } else {
27 std::cout << "Error: " << result.error() << '\n';
28 }
29
30 auto error_result = safe_divide(10.0, 0.0)
31 .and_then(safe_sqrt);
32
33 if (!error_result) {
34 std::cout << "Error: " << error_result.error() << '\n';
35 }
36
37 return 0;
38}
System Console

Waiting for signal...