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

<system_error>

Portable, extensible error-code infrastructure for reporting OS and library errors as values rather than exceptions, introduced in C++11.

<system_error>since C++11

A header providing std::error_code, std::error_condition, std::error_category, std::system_error, and std::errc β€” a structured, extensible mechanism for representing and transporting OS-level and library-level error codes without requiring exceptions.

Overview

Before C++11, error propagation from system calls was a patchwork: POSIX errno, Windows HRESULT, GetLastError(), and ad hoc integer returns with no common comparison semantics. <system_error> unifies these under a single abstraction without hiding platform specifics.

The design centres on three cooperating types:

  • std::error_category β€” a singleton that names a domain of error codes and maps integer values to human-readable messages. You derive from this to define your own error domain.
  • std::error_code β€” a lightweight value (integer + pointer to category) representing a concrete, platform-specific error. This is what a function returns or stores.
  • std::error_condition β€” a portable representation of an error class, used for comparisons across categories. std::errc values are mapped to error conditions via generic_category().

The separation of error_code (specific) from error_condition (portable) is the key insight: code on POSIX stores errno ENOENT (value 2, system_category) but you can compare it portably against std::errc::no_such_file_or_directory.

Four standard categories ship with the library:

FunctionCategoryEnum
std::generic_category()POSIX errno valuesstd::errc
std::system_category()Platform-native codesβ€”
std::iostream_category()<ios> stream errorsstd::io_errc
std::future_category()<future> errorsstd::future_errc

Syntax

cpp
#include <system_error>

// Error code construction
std::error_code ec;                                          // value 0, system_category β€” "no error"
std::error_code ec1{ errno, std::generic_category() };      // from POSIX errno
std::error_code ec2 = std::make_error_code(std::errc::permission_denied);

// Inspection
if (ec) { /* non-zero means error */ }
int  val  = ec.value();
auto& cat = ec.category();           // reference to singleton
std::string msg = ec.message();

// Portable comparison
ec2 == std::errc::permission_denied; // true β€” cross-category via default_error_condition()

// Throwing
throw std::system_error(ec2, "open failed");

// Catching
try { /* ... */ }
catch (const std::system_error& e) {
    std::error_code code = e.code();
    std::string text     = e.what();  // message() + what_arg concatenated
}

Examples

Wrapping a POSIX call without throwing

Many APIs accept an std::error_code& output parameter as an alternative to throwing. Implement the same pattern in your own code:

cpp
#include <system_error>
#include <cerrno>
#include <fcntl.h>
#include <unistd.h>

int open_file(const char* path, std::error_code& ec) noexcept {
    int fd = ::open(path, O_RDONLY);
    if (fd == -1) {
        ec.assign(errno, std::system_category());
        return -1;
    }
    ec.clear();   // explicit success signal
    return fd;
}

// Throwing overload delegates to the non-throwing one
int open_file(const char* path) {
    std::error_code ec;
    int fd = open_file(path, ec);
    if (ec) throw std::system_error(ec, path);
    return fd;
}

Both overloads share the same implementation body β€” a pattern used throughout <filesystem> since C++17.

Portable error comparison

cpp
#include <system_error>
#include <cerrno>

void handle(std::error_code ec) {
    // Compare against portable errc β€” works regardless of which category ec came from
    if (ec == std::errc::no_such_file_or_directory) {
        // ENOENT on POSIX, ERROR_FILE_NOT_FOUND on Windows if system_category maps it
    } else if (ec == std::errc::permission_denied) {
        // EACCES / ERROR_ACCESS_DENIED
    } else if (ec) {
        // Some other error
        std::fprintf(stderr, "[%s] %s (%d)\n",
            ec.category().name(), ec.message().c_str(), ec.value());
    }
}

Defining a custom error category

Extend error_category to integrate a third-party library or your own subsystem:

cpp
#include <system_error>
#include <string>

enum class net_error {
    success           = 0,
    connection_refused = 1,
    timeout            = 2,
    protocol_error     = 3,
};

struct net_error_category : std::error_category {
    const char* name() const noexcept override { return "net"; }

    std::string message(int ev) const override {
        switch (static_cast<net_error>(ev)) {
            case net_error::connection_refused: return "connection refused";
            case net_error::timeout:            return "operation timed out";
            case net_error::protocol_error:     return "protocol violation";
            default:                            return "unknown net error";
        }
    }

    // Optional: map to portable conditions so callers can use errc comparisons
    std::error_condition default_error_condition(int ev) const noexcept override {
        if (ev == static_cast<int>(net_error::connection_refused))
            return std::errc::connection_refused;
        return std::error_condition{ ev, *this };
    }
};

// Singleton accessor β€” must outlive all error_code objects using it
const std::error_category& net_category() noexcept {
    static net_error_category instance;
    return instance;
}

// ADL hook β€” enables make_error_code(net_error::timeout)
std::error_code make_error_code(net_error e) noexcept {
    return { static_cast<int>(e), net_category() };
}

// Opt in to implicit conversion so net_error can initialise error_code directly
template <>
struct std::is_error_code_enum<net_error> : std::true_type {};

// Usage
std::error_code ec = net_error::timeout;
assert(ec == std::errc::connection_refused || ec.category() == net_category());

The is_error_code_enum specialisation (C++11) enables std::error_code ec = net_error::timeout; without an explicit make_error_code call at the use site.

Integration with std::expected (C++23)

std::expected<T, std::error_code> is the modern alternative to the output-parameter pattern. Instead of returning a value and writing an error into a reference parameter, the function returns a single object that is either the value or the error β€” modelled after Rust's Result type.

Converting the output-parameter pattern to std::expected:

cpp
#include <expected>
#include <system_error>
#include <cerrno>
#include <fcntl.h>
#include <unistd.h>

// Old style: value + error_code& output parameter
int open_file(const char* path, std::error_code& ec) noexcept;

// New style: expected encodes both outcomes in the return type
std::expected<int, std::error_code> open_file_ex(const char* path) noexcept {
    int fd = ::open(path, O_RDONLY);
    if (fd == -1)
        return std::unexpected(std::error_code(errno, std::system_category()));
    return fd;
}

The caller no longer needs to declare a separate error_code variable and check it after the call:

cpp
// Error_code style
std::error_code ec;
int fd = open_file("/etc/hosts", ec);
if (ec) { /* handle */ }

// expected style β€” error cannot be silently ignored
auto result = open_file_ex("/etc/hosts");
if (!result) {
    std::fprintf(stderr, "open: %s\n", result.error().message().c_str());
    return;
}
int fd = *result;

Chaining with monadic operations (C++23):

std::expected supports and_then, transform, and or_else, enabling pipeline-style composition where errors short-circuit automatically β€” without nested if checks:

cpp
std::expected<std::string, std::error_code> read_first_line(const char* path) noexcept;
std::expected<int,         std::error_code> parse_int(std::string s) noexcept;

// Chains: open β†’ read β†’ parse; first failure stops the chain
auto value = open_file_ex("/etc/myconfig")
    .and_then([](int fd) { return read_first_line_from_fd(fd); })
    .and_then([](std::string s) { return parse_int(std::move(s)); })
    .value_or(-1);  // provide a default if any step failed

Each and_then lambda is only invoked when the previous step held a value; on error it forwards the error_code unchanged. Use transform for infallible transformations and or_else for recovery.

std::error_code is the recommended error type for std::expected in library APIs: it is cheap (8–16 bytes), carries category information, and interoperates with std::system_error for callers who prefer exceptions.

error_code vs error_condition: When to Use Which

These two types serve distinct roles in the <system_error> design:

std::error_codestd::error_condition
PurposeStore and transmit a concrete error, unchangedPerform a portable query / classification over error codes
Platform dependencePlatform-specific (the raw OS value)Platform-independent (an abstracted condition)
What functions returnYes β€” functions produce error_codeNo β€” conditions are used for comparisons only
Use in if comparisonsUse when checking exact codes from a known categoryUse when checking portably (e.g. errc::no_such_file_or_directory)
Registration traitstd::is_error_code_enum<E>std::is_error_condition_enum<E>
Conversion functionmake_error_code(E)make_error_condition(E)

Rule of thumb: functions return error_code; callers compare against error_condition (or errc values, which are conditions).

cpp
// Functions produce error_code:
std::error_code ec = open_file("/etc/passwd", ec);

// Portable comparison uses error_condition (via errc):
if (ec == std::errc::permission_denied)     // portable β€” works on POSIX and Windows
    std::puts("access denied");
if (ec == std::error_code{EACCES, std::generic_category()})  // fragile β€” POSIX only
    std::puts("access denied");

Defining a portable condition enum with is_error_condition_enum:

When multiple subsystem-specific error codes all mean the same thing conceptually, group them under a custom error_condition instead of comparing each one individually:

cpp
enum class AppCondition {
    // no 0
    bad_user_input = 1,   // matches any "user did something wrong" code
    system_failure = 2,   // matches internal/infrastructure errors
};

namespace std {
    template<>
    struct is_error_condition_enum<AppCondition> : true_type {};
}

std::error_condition make_error_condition(AppCondition c) noexcept;

// Category implements equivalent() to define the mapping:
struct AppConditionCategory : std::error_category {
    const char* name() const noexcept override { return "app-condition"; }
    std::string message(int ev) const override { /* ... */ return ""; }

    bool equivalent(const std::error_code& ec, int cond) const noexcept override {
        switch (static_cast<AppCondition>(cond)) {
        case AppCondition::bad_user_input:
            return ec == std::errc::invalid_argument
                || ec == std::errc::argument_out_of_domain;
        case AppCondition::system_failure:
            return ec == std::errc::io_error
                || ec == std::errc::no_space_on_device;
        default: return false;
        }
    }
};

// Now callers can group-test across many codes with one comparison:
std::error_code ec = /* ... some library call ... */;
if (ec == AppCondition::bad_user_input)
    show_user_error(ec.message());
else if (ec == AppCondition::system_failure)
    log_and_retry(ec);

operator== between error_code and error_condition calls both equivalent overloads from both categories, so a new error code enum can opt in to existing conditions without modifying the condition's category.

Pattern: Asynchronous Error Propagation

Callback-based async APIs popularised by Asio establish a canonical handler signature where error_code is always the first argument:

cpp
// The Asio async handler signature:
// void handler(std::error_code ec, ResultType result);

void async_read_handler(std::error_code ec, std::size_t bytes_transferred) {
    if (ec) {
        // ec == asio::error::eof signals clean close, not a bug
        if (ec == asio::error::eof)
            handle_disconnect();
        else
            log_error(ec);
        return;
    }
    process_data(bytes_transferred);
}

Passing error_code by value to callbacks is intentional: it is small (typically 8–16 bytes), trivially copyable, and requires no heap allocation. Unlike exceptions, it does not unwind the stack β€” the callback simply checks the value at the top of its body.

Coroutine-based async (C++20 / Asio use_awaitable):

When using C++20 coroutines with Asio, the default use_awaitable token converts a failing error_code into a std::system_error exception at the co_await point:

cpp
// use_awaitable: errors become exceptions β€” clean, linear code
asio::awaitable<void> echo(tcp::socket socket) {
    try {
        char buf[1024];
        for (;;) {
            std::size_t n = co_await socket.async_read_some(
                asio::buffer(buf), asio::use_awaitable);
            co_await async_write(socket, asio::buffer(buf, n), asio::use_awaitable);
        }
    } catch (const std::system_error& e) {
        if (e.code() != asio::error::eof)        // distinguish clean close from real error
            log_error(e.code());
    }
}

To suppress exception conversion and receive the error_code directly in coroutine code, use as_tuple or redirect_error:

cpp
// as_tuple: error_code returned directly β€” no exception thrown
asio::awaitable<void> echo_nothrow(tcp::socket socket) {
    char buf[1024];
    for (;;) {
        auto [ec, n] = co_await socket.async_read_some(
            asio::buffer(buf),
            asio::as_tuple(asio::use_awaitable));
        if (ec) break;   // handle ec without try/catch
        co_await async_write(socket, asio::buffer(buf, n), asio::use_awaitable);
    }
}

The choice between the two is a style decision: use_awaitable keeps coroutine bodies linear but requires try/catch; as_tuple / redirect_error make error handling explicit at every co_await site, mirroring the callback-era pattern inside coroutine syntax.

Best Practices

Provide both throwing and non-throwing overloads. Follow the <filesystem> convention: the std::error_code& overload never throws; the throwing overload wraps it. Callers choose their error-handling style; you maintain one implementation.

Always call ec.clear() on success. If the caller reuses an error_code across calls, a stale error from a previous failure will persist unless you explicitly reset it.

Return error_code by value; take it by & in output-parameter style. Don't store pointers or references β€” categories are singletons, codes are cheap value types (typically 8–16 bytes).

Use generic_category() for POSIX errno, system_category() for native OS codes. On POSIX they may be the same category, but on Windows system_category() represents DWORD error codes while generic_category() retains POSIX semantics. Mixing them produces wrong comparisons.

Keep category singletons in translation units, not headers. The singleton must have a unique address across the entire program. A static local in an inline function creates one per TU on some ABIs β€” instead, define the accessor in a .cpp file and declare it extern in the header.

Common Pitfalls

Comparing codes from different categories directly by value. error_code{2, system_category()} == error_code{2, generic_category()} is false even if both mean ENOENT. Always compare via errc values or default_error_condition().

Assuming message() is thread-safe. The standard only requires error_category::message() to be semantically const, not internally lock-free. On some Windows implementations it calls FormatMessage, which caches a thread-local buffer. Fine in practice, but be aware.

Omitting noexcept from error_category overrides. name() and operator==() in error_category are noexcept. If your override can throw, you have undefined behaviour when called from a noexcept path (e.g., inside a destructor formatting an error message).

Throwing std::system_error with a zero error code. A zero-valued error_code conventionally means success. Throwing with it produces a what() of just your message string with no error description appended β€” confusing to callers.

Custom Error Categories In Depth

Implementing std::error_category correctly requires attention to three responsibilities: naming, message mapping, and the global singleton guarantee. The name returned by name() appears in diagnostic output and must uniquely identify the domain; the integer values you assign to your enum are meaningful only within that category's namespace.

The singleton pattern is non-negotiable. Every std::error_code stores a raw pointer to its category, so the object must remain valid for the lifetime of the program. The safest idiom is a function-local static:

cpp
// C++11
#include <system_error>
#include <string>

enum class db_error {
    ok              = 0,
    connection_lost = 1,
    query_syntax    = 2,
    constraint      = 3,
    not_found       = 4,
};

struct db_error_category final : std::error_category {
    const char* name() const noexcept override { return "db"; }

    std::string message(int ev) const override {
        switch (static_cast<db_error>(ev)) {
        case db_error::connection_lost: return "database connection lost";
        case db_error::query_syntax:    return "SQL syntax error";
        case db_error::constraint:      return "constraint violation";
        case db_error::not_found:       return "record not found";
        default:                        return "unknown database error";
        }
    }

    // Map domain-specific codes to portable errc conditions where it makes sense
    std::error_condition
    default_error_condition(int ev) const noexcept override {
        switch (static_cast<db_error>(ev)) {
        case db_error::not_found:
            return std::errc::no_such_file_or_directory; // conceptually "missing"
        case db_error::connection_lost:
            return std::errc::connection_aborted;
        default:
            return std::error_condition{ ev, *this };
        }
    }
};

// Defined in a .cpp file β€” NOT inline, to guarantee a single object address
const std::error_category& db_category() noexcept {
    static db_error_category instance;
    return instance;
}

std::error_code make_error_code(db_error e) noexcept {
    return { static_cast<int>(e), db_category() };
}

template <>
struct std::is_error_code_enum<db_error> : std::true_type {};

With is_error_code_enum registered, user code can write std::error_code ec = db_error::not_found; directly, and ec == std::errc::no_such_file_or_directory returns true via the default_error_condition mapping.

std::system_error in File and Network Code

POSIX system calls signal failure through a global errno variable that is overwritten by any subsequent call β€” capture it into std::error_code immediately. The pattern shown below prevents the common bug where errno is read after a logging call has silently clobbered it.

cpp
// C++11
#include <system_error>
#include <cerrno>
#include <cstdio>

struct FileDeleter {
    void operator()(FILE* f) const noexcept { if (f) std::fclose(f); }
};
using UniqueFile = std::unique_ptr<FILE, FileDeleter>;

UniqueFile open_config(const char* path, std::error_code& ec) noexcept {
    FILE* f = std::fopen(path, "r");
    if (!f) {
        // Capture errno BEFORE any other call
        ec.assign(errno, std::generic_category());
        return nullptr;
    }
    ec.clear();
    return UniqueFile{ f };
}

Throwing the std::system_error overload is equally concise. The constructor concatenates the category message and the caller-supplied context string in what(), which is what log files and crash reporters will see:

cpp
// C++11
#include <system_error>
#include <string>

void must_open(const std::string& path) {
    std::error_code ec;
    auto f = open_config(path.c_str(), ec);
    if (ec)
        throw std::system_error(ec, "open_config(" + path + ")");
    // what() yields e.g.: "open_config(/etc/app.conf): No such file or directory"
}

Catching std::system_error at a higher layer lets you inspect both the human-readable string and the structured code without string parsing:

cpp
// C++11
#include <system_error>
#include <cstdio>

void load_configuration(const char* path) {
    try {
        must_open(path);
    } catch (const std::system_error& e) {
        std::error_code ec = e.code();
        if (ec == std::errc::no_such_file_or_directory) {
            std::fprintf(stderr, "Config not found at %s β€” using defaults\n", path);
        } else if (ec == std::errc::permission_denied) {
            std::fprintf(stderr, "Permission denied reading %s\n", path);
        } else {
            // Re-throw unexpected errors; don't swallow them
            throw;
        }
    }
}

The same structure applies to network code: BSD socket calls set errno on failure, so the exact same ec.assign(errno, std::generic_category()) idiom works for connect(), send(), recv(), and bind().

std::error_condition for Cross-Platform Portable Checks

std::error_condition lets you write comparison logic that is independent of whether the underlying OS uses POSIX errno, Windows DWORD, or a library-defined integer. The std::errc enum provides named constants for all POSIX error conditions; comparing an error_code against an errc value triggers default_error_condition() inside operator==, resolving the platform mapping transparently.

cpp
// C++11
#include <system_error>
#include <cerrno>

// Simulate an OS error from any platform
std::error_code ec{ errno, std::system_category() };

// These comparisons are portable β€” no #ifdef needed
bool missing  = (ec == std::errc::no_such_file_or_directory);
bool denied   = (ec == std::errc::permission_denied);
bool busy     = (ec == std::errc::device_or_resource_busy);
bool would_block = (ec == std::errc::operation_would_block);

When you need to distinguish "file not found" from "path component not found", use std::error_condition explicitly to document intent:

cpp
// C++17
#include <system_error>
#include <filesystem>

void report(std::error_code ec) {
    std::error_condition cond = ec.default_error_condition();

    // Inspect the portable condition rather than the raw platform value
    if (cond == std::errc::no_such_file_or_directory) {
        // Works whether ec came from open(), stat(), std::filesystem, or a custom category
        std::puts("entity does not exist");
    } else if (cond == std::errc::not_a_directory) {
        std::puts("path component is not a directory");
    } else if (cond == std::errc::file_exists) {
        std::puts("target already exists");
    } else {
        std::fprintf(stderr, "[%s:%d] %s\n",
            ec.category().name(), ec.value(), ec.message().c_str());
    }
}

std::filesystem (C++17) returns error_code values from filesystem_category(), but the standard mandates that they compare correctly against std::errc values, so the above function works unchanged whether you pass a POSIX open error or a std::filesystem::copy error.

See Also

  • std::errc β€” scoped enum mapping POSIX errno names to portable error_condition values (C++11, <system_error>)
  • std::filesystem::filesystem_error β€” specialised system_error subclass for filesystem failures (C++17, <filesystem>)
  • std::future_error β€” system_error subclass thrown by broken promises and invalid future state (C++11, <future>)
  • std::expected<T, E> β€” C++23 alternative that encodes success/failure in the return type without needing a separate output parameter (<expected>)