Modern Error Handling

Modern Error Handling

Model absence, alternatives, and recoverable failures with modern standard-library types.

Modern Error Handling

Why this matters

Many codebases still jump straight from raw return codes to exceptions. Modern C++ gives you more expressive choices:

Choosing the right model makes APIs easier to read and test.

std::optional

std::optional<int> find_port(std::string_view service_name) {
    if (service_name == "http") {
        return 80;
    }
    return std::nullopt;
}

Use optional when absence is expected and there is no need to explain why the value is missing.

A realistic pattern

std::optional<std::string_view> lookup_env(std::string_view key) {
    if (key == "MODE") {
        return "release";
    }
    return std::nullopt;
}

This style is useful when "not found" is a normal outcome and the caller can choose a default.

std::variant

using Token = std::variant<int, std::string>;

variant works well when multiple result shapes are all valid domain states.

std::expected

enum class ParseError {
    empty_input,
    invalid_digit,
    overflow,
};

std::expected<int, ParseError> parse_int(std::string_view text) {
    if (text.empty()) {
        return std::unexpected(ParseError::empty_input);
    }
    return 42;
}

Now the caller handles success and failure explicitly:

if (auto value = parse_int(input)) {
    std::cout << "parsed: " << *value << '\n';
} else {
    std::cout << "parse failed\n";
}

Error propagation

std::expected<int, ParseError> load_port(std::string_view text) {
    auto parsed = parse_int(text);
    if (!parsed) {
        return std::unexpected(parsed.error());
    }

    if (*parsed < 1 || *parsed > 65535) {
        return std::unexpected(ParseError::overflow);
    }

    return *parsed;
}

The benefit is that both success and error paths remain visible in normal code flow.

Exceptions vs expected

Best practices

Exercises