Modern Error Handling

Modern Error Handling

Use optional, variant, expected, exceptions, and result-oriented APIs deliberately.

Modern Error Handling

Choose the right tool

std::optional

std::optional<int> find_id(std::string_view name);

if (auto id = find_id("ada")) {
    std::cout << *id << '\n';
}

Use it for "maybe there is a value". Do not overload it with detailed failure reasons.

std::optional<User> find_user(Id id);

If callers always need to know why lookup failed, optional is the wrong type.

std::expected

enum class ParseError { invalid_digit, overflow };

std::expected<int, ParseError> parse_port(std::string_view text);

std::expected keeps the success path explicit and makes error propagation testable.

auto port = parse_port(text);
if (!port) {
    return std::unexpected(port.error());
}

std::variant

using ConfigValue = std::variant<int, bool, std::string>;

Visit a variant when the set of alternatives is fixed and meaningful.

std::visit([](const auto& value) {
    handle(value);
}, config_value);

Practical guidance

Layering rule of thumb

Quick selection rules

Pitfalls to avoid

Example in practice

#include <optional>

std::optional<int> value() {
    return 42;
}

Try this variation

Return the error reason instead of only success or failure. That makes the tradeoff between `optional`, `expected`, and exceptions much clearer.

#include <expected>
#include <string>

std::expected<int, std::string> parse(bool ok) {
    if (!ok) {
        return std::unexpected("invalid input");
    }
    return 42;
}