<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++11A 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::errcvalues are mapped to error conditions viageneric_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:
| Function | Category | Enum |
|---|---|---|
std::generic_category() | POSIX errno values | std::errc |
std::system_category() | Platform-native codes | β |
std::iostream_category() | <ios> stream errors | std::io_errc |
std::future_category() | <future> errors | std::future_errc |
Syntax
#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:
#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
#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:
#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:
#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:
// 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:
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 failedEach 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_codeis the recommended error type forstd::expectedin library APIs: it is cheap (8β16 bytes), carries category information, and interoperates withstd::system_errorfor 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_code | std::error_condition | |
|---|---|---|
| Purpose | Store and transmit a concrete error, unchanged | Perform a portable query / classification over error codes |
| Platform dependence | Platform-specific (the raw OS value) | Platform-independent (an abstracted condition) |
| What functions return | Yes β functions produce error_code | No β conditions are used for comparisons only |
Use in if comparisons | Use when checking exact codes from a known category | Use when checking portably (e.g. errc::no_such_file_or_directory) |
| Registration trait | std::is_error_code_enum<E> | std::is_error_condition_enum<E> |
| Conversion function | make_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).
// 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:
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:
// 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:
// 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:
// 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:
// 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.
// 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:
// 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:
// 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.
// 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:
// 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 portableerror_conditionvalues (C++11,<system_error>)std::filesystem::filesystem_errorβ specialisedsystem_errorsubclass for filesystem failures (C++17,<filesystem>)std::future_errorβsystem_errorsubclass 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>)