Skip to content
C++
Library
since C++23
Intermediate

std::expected

C++23 vocabulary type holding either a success value T or an error value E, making failure modes explicit in the return type without exceptions.

std::expected<T, E>since C++23

A sum type that holds either a value of type T (success) or a value of type E (error), making failure modes explicit in the function signature without exceptions or out-parameters.

Overview

std::expected<T, E> is a vocabulary type β€” a standard representation for "this operation either produces a T or fails with an E", recognised across codebases without custom result wrappers. It occupies the same design space as Rust's Result<T, E>, but integrates cleanly with C++ value semantics, move semantics, and the existing <system_error> infrastructure.

The key distinction from std::optional<T> is that the absent case carries information β€” the error value tells the caller why the operation failed, not just that it did.

Reach for expected when:

  • A function performs fallible I/O, parsing, or network operations and the caller needs to act on the failure reason
  • You want the type system to force acknowledgment of failure paths
  • You are building pipelines where multiple operations chain and errors propagate linearly
  • Exceptions are off-limits (embedded, real-time, or policy constraints)

Do not use it for:

  • Programmer errors (precondition violations, out-of-bounds) β€” use assert or terminate
  • Code already built around exceptions, where mixing models adds friction
  • Simple optional returns with no meaningful error β€” std::optional is the right vocabulary there

Syntax

cpp
#include <expected>              // C++23

// Primary template
template<class T, class E>
class std::expected;

// Void specialisation β€” success/failure only, no value produced
template<class E>
class std::expected<void, E>;

// Sentinel wrapper for constructing the error state
template<class E>
class std::unexpected;           // C++23

// In-place construction tags
std::in_place                    // constructs T in-place (shared with optional/variant)
std::unexpect                    // constructs E in-place (type: std::unexpect_t, C++23)

Type constraints: T must be an object type (possibly cv-qualified) or void. E must be a destructible, non-array object type β€” void is not permitted as E; use std::monostate if no error data is needed.

Key members

MemberDescription
has_value() / operator bool()true if the object holds a value
operator*(), operator->()Access value β€” UB if !has_value()
value()Access value β€” throws std::bad_expected_access<E> if !has_value()
error()Access error β€” precondition: !has_value()
value_or(u)Return value or fallback u
emplace(args...)Destroy current state; construct T in-place from args; return T&
and_then(f)If value: invoke f(*val) β†’ expected<U,E>; otherwise pass error through
transform(f)If value: wrap f(*val) in expected<U,E>; otherwise pass error through
or_else(f)If error: invoke f(err) β†’ expected<T,G>; otherwise pass value through
transform_error(f)Map error E β†’ G; leave value unchanged

Examples

Layered file parsing

cpp
#include <expected>
#include <filesystem>
#include <fstream>
#include <sstream>
#include <system_error>
#include <charconv>

enum class ParseError { EmptyInput, InvalidFormat, OutOfRange };

std::expected<std::string, std::error_code>
read_file(const std::filesystem::path& p) {
    std::ifstream f(p);
    if (!f)
        return std::unexpected(std::make_error_code(std::errc::no_such_file_or_directory));
    std::ostringstream ss;
    ss << f.rdbuf();
    return ss.str();
}

std::expected<int, ParseError>
parse_port(std::string_view s) {
    if (s.empty()) return std::unexpected(ParseError::EmptyInput);
    int n = 0;
    auto [ptr, ec] = std::from_chars(s.data(), s.data() + s.size(), n);
    if (ec != std::errc{}) return std::unexpected(ParseError::InvalidFormat);
    if (n < 1 || n > 65535) return std::unexpected(ParseError::OutOfRange);
    return n;
}

Manual propagation vs. monadic chaining

Both are valid. Manual propagation is explicit and easy to annotate with logging or branching. Monadic chaining eliminates boilerplate for truly linear pipelines.

cpp
// Manual β€” clear, debuggable, easy to extend
std::expected<Config, AppError> load_config(std::string_view path) {
    auto text = read_file(path);
    if (!text) return std::unexpected(to_app_error(text.error()));

    auto doc = parse_json(*text);
    if (!doc) return std::unexpected(doc.error());

    return validate(*doc);
}

// Monadic β€” concise when every step is a pure transformation
std::expected<Config, AppError> load_config_v2(std::string_view path) {
    return read_file(path)
        .transform_error(to_app_error)    // std::error_code β†’ AppError
        .and_then(parse_json)             // string β†’ expected<JsonDoc, AppError>
        .and_then(validate);              // JsonDoc β†’ expected<Config, AppError>
}

transform and transform_error

cpp
// transform maps the value; transform_error maps the error type
auto result =
    parse_port(user_input)                          // expected<int, ParseError>
    .transform_error([](ParseError e) {             // C++23
        return AppError{static_cast<int>(e), "port parse failed"};
    })                                              // β†’ expected<int, AppError>
    .transform([](int port) {
        return std::format("connecting to :{}", port);
    });                                             // β†’ expected<string, AppError>

or_else for partial recovery

cpp
auto port =
    parse_port(user_input)
    .or_else([](ParseError e) -> std::expected<int, ParseError> {
        if (e == ParseError::OutOfRange) return 80;  // recover with default
        return std::unexpected(e);                   // re-raise anything else
    });

expected<void, E> β€” operations with no success value

cpp
std::expected<void, std::error_code>
write_file(const std::filesystem::path& p, std::string_view data) {
    std::ofstream f(p);
    if (!f) return std::unexpected(std::make_error_code(std::errc::permission_denied));
    f << data;
    if (!f) return std::unexpected(std::make_error_code(std::errc::io_error));
    return {};   // success: value-initialised void
}

// Monadic chaining works identically on the void specialisation
auto r = write_file("out.txt", content)
    .and_then([&] { return write_file("out.bak", content); })
    .transform_error([](std::error_code ec) { return AppError{ec.message()}; });

In-place construction and emplace

cpp
// Construct T without copies when T has a multi-arg constructor
std::expected<std::vector<int>, std::string> r{std::in_place, {1, 2, 3}};

// Construct E in-place with std::unexpect
std::expected<int, std::string> err{std::unexpect, "bad input"};

// Replace current state in-place β€” avoids an extra copy/move
r.emplace(10, 0);   // constructs vector<int>(10, 0); returns vector<int>&

Best Practices

Pick an error type that matches caller needs. std::error_code integrates with <system_error> for POSIX/OS errors and is cheap to copy. A scoped enum class works well for domain-specific failures. std::string loses machine-readable structure and should be a last resort. Over-engineered error hierarchies are as harmful as under-engineered ones β€” model only the distinctions callers actually make.

Push value_or to the boundary. Deep inside domain logic, acknowledge errors explicitly. Convert to a default only at the edge β€” a UI renderer, a serialiser, a log line β€” where a fallback is genuinely correct.

Remember that expected does not propagate automatically. Unlike exceptions, an error sitting in an expected stays put until code explicitly handles or forwards it. Design error paths intentionally: propagate with and_then, return early with if (!r) return std::unexpected(r.error()), or recover with or_else.

Prefer monadic chaining for pure pipelines; manual propagation when you need side effects. and_then and transform cannot easily log, mutate state, or branch mid-pipeline. A plain if (!r) guard is often clearer and more maintainable when the pipeline is not purely functional.

Common Pitfalls

Forgetting std::unexpected

cpp
// Does not compile β€” std::string cannot implicitly construct the error state
std::expected<int, std::string> r = "error message";

// Correct
std::expected<int, std::string> r = std::unexpected(std::string{"error message"});

Accessing value or error without checking

cpp
std::expected<int, std::string> r = std::unexpected("bad");

int v  = *r;        // UB β€” precondition: has_value()
int v2 = r.value(); // throws std::bad_expected_access<std::string>

std::string e = r.error(); // OK β€” precondition satisfied

operator*, operator->, and error() carry preconditions, not safety nets. Only value() converts a violated precondition into a catchable exception; the others invoke undefined behaviour when misused. In hot paths, check first and access with operator*.

Using expected for programmer errors

A violated precondition β€” null pointer, out-of-bounds index, wrong argument type β€” is a bug. Encoding bugs as std::unexpected trains callers to check for bugs rather than eliminate them. Use assert, [[unlikely]] with terminate, or forthcoming C++ contracts for precondition enforcement.

Mixing expected and exceptions in the same layer

expected-based and exception-based code can coexist in a codebase but not within the same call chain without an explicit adapter. Pick a single error-propagation model per layer and convert at the boundaries.

See Also

  • std::optional β€” value-or-nothing without a typed error
  • std::variant β€” general discriminated union; expected is a specialised variant
  • Error handling β€” comparing exceptions, error codes, and expected
  • std::error_code β€” composable OS and library error values, a natural E choice