C++ by Example
C++ by Example

std::optional and std::expected

2 min read

std::optional and std::expected

Not all errors are exceptional. When a function might legitimately fail to produce a value — parsing a number, looking up a key, opening a file — using exceptions for the failure case is heavyweight. std::optional and std::expected model this in the type system without throwing.

std::optional (C++17)

std::optional<T> holds either a T value or nothing (std::nullopt):

#include <optional>
#include <string>
#include <iostream>

std::optional<int> parse_int(const std::string& s) {
    try {
        return std::stoi(s);
    } catch (...) {
        return std::nullopt;
    }
}

int main() {
    auto a = parse_int("42");
    auto b = parse_int("hello");

    if (a) std::cout << "parsed: " << *a << "\n";   // parsed: 42
    if (!b) std::cout << "parse failed\n";            // parse failed

    // Safe access with value_or
    std::cout << a.value_or(-1) << "\n";   // 42
    std::cout << b.value_or(-1) << "\n";   // -1

    // Throws std::bad_optional_access if empty
    // std::cout << b.value() << "\n";
}

Chaining with optional

std::optional pairs naturally with early returns to keep code flat:

std::optional<std::string> read_env(const std::string& key) {
    const char* val = std::getenv(key.c_str());
    if (!val) return std::nullopt;
    return std::string(val);
}

std::optional<int> get_port() {
    auto s = read_env("PORT");
    if (!s) return std::nullopt;
    return parse_int(*s);
}

std::expected (C++23)

std::expected<T, E> carries either a success value of type T or an error value of type E. Unlike optional, it tells you why the operation failed:

#include <expected>
#include <string>
#include <system_error>

std::expected<int, std::string> parse_port(const std::string& s) {
    try {
        int n = std::stoi(s);
        if (n < 1 || n > 65535)
            return std::unexpected("port out of range: " + s);
        return n;
    } catch (...) {
        return std::unexpected("not a number: " + s);
    }
}

int main() {
    auto r = parse_port("8080");
    if (r) {
        std::cout << "port: " << *r << "\n";
    } else {
        std::cout << "error: " << r.error() << "\n";
    }

    auto bad = parse_port("99999");
    std::cout << bad.value_or(0) << "\n";   // 0 — fallback on error
}

std::expected with error codes

Using an enum for the error type gives structured, matchable errors:

enum class ParseError { EmptyInput, InvalidChar, Overflow };

std::expected<int, ParseError> parse_strict(const std::string& s) {
    if (s.empty()) return std::unexpected(ParseError::EmptyInput);
    for (char c : s) if (!std::isdigit(c)) return std::unexpected(ParseError::InvalidChar);
    try { return std::stoi(s); }
    catch (...) { return std::unexpected(ParseError::Overflow); }
}

When to use what

Situation Use
Value might be absent, no reason needed std::optional<T>
Value might be absent, reason matters std::expected<T, E>
Truly exceptional / unexpected failure exceptions
Performance-critical hot path, known failure modes error codes / return values

Prefer optional and expected for ordinary failure modes — file not found, key not in map, invalid input. Reserve exceptions for truly unexpected situations: allocation failure, violated class invariants, I/O errors on required resources.

C++ by Example
C++ by Example

Learn modern C++ through working code. Each chapter introduces one concept — variables, functions, classes, templates, smart pointers, concurrency — with a clear example, a line-by-line explanation, and notes on how it applies in real programs.

View book →