Error Handling
|
This section documents C++23 (ISO/IEC 14882:2024), as published by ISO/IEC JTC1/SC22/WG21 (wg21), verified against the freely available working draft N5046 (eel.is/c++draft) and cppreference.com. This content was generated with the assistance of AI and should be verified against the working draft and cppreference.com before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
Exceptions and the Standard Hierarchy
Every standard exception derives from std::exception, so catch (const std::exception& e) catches anything
standard-library code throws, and e.what() gives a human-readable message:
#include <stdexcept>
// std::exception
// +-- std::logic_error (a programming mistake, in principle detectable before running)
// | +-- std::invalid_argument, std::out_of_range, std::length_error, std::domain_error
// +-- std::runtime_error (only detectable at run time)
// | +-- std::range_error, std::overflow_error, std::underflow_error, std::system_error
// +-- std::bad_alloc, std::bad_cast, std::bad_variant_access, std::bad_optional_access, ...
class ConfigError : public std::runtime_error { // custom exceptions should derive from a standard base
public:
explicit ConfigError(const std::string& msg) : std::runtime_error(msg) {}
};
throw/try/catch
#include <stdexcept>
#include <iostream>
double divide(double a, double b) {
if (b == 0.0) throw std::invalid_argument("division by zero");
return a / b;
}
int main() {
try {
divide(1.0, 0.0);
} catch (const std::invalid_argument& e) { // most-derived/most-specific catch clauses first
std::cout << "invalid argument: " << e.what() << '\n';
} catch (const std::exception& e) { // broader fallback
std::cout << "error: " << e.what() << '\n';
} catch (...) { // catches literally anything, even non-std::exception types
std::cout << "unknown error\n";
}
}
Exception Safety Guarantees
Code offering the strong guarantee either fully succeeds or leaves state exactly as it was (no partial
effects); the basic guarantee promises no leaks/corruption but state may have partially changed; no-throw
(noexcept) promises the operation never throws at all:
#include <vector>
class Transaction {
public:
// Two members must change together, or not at all -- that is what actually motivates the idiom here
// (`vector::push_back` on its own already gives the strong guarantee).
void addItem(int item, int cost) {
auto updated = items_; // copy-and-swap: do the work on a copy, so if push_back throws,
updated.push_back(item); // both members below are untouched and the commit never runs
const int updatedTotal = total_ + cost;
items_.swap(updated); // commit: swap is noexcept and the int assignment cannot throw,
total_ = updatedTotal; // so the object is never left half-updated
}
private:
std::vector<int> items_;
int total_ = 0;
};
noexcept
Covered in Functions and Lambdas; a noexcept
function that does throw calls std::terminate immediately — there is no unwinding, no catch clause ever
sees it.
std::exception_ptr
Captures an in-flight exception so it can be stored, transferred across threads, and re-thrown later — the mechanism behind `std::future’s "an exception from another thread surfaces when you call `.get()`":
#include <exception>
#include <iostream>
std::exception_ptr captured;
void risky() { throw std::runtime_error("boom"); }
int main() {
try {
risky();
} catch (...) {
captured = std::current_exception(); // capture without handling it here
}
if (captured) {
try {
std::rethrow_exception(captured); // re-throw it later, possibly on a different thread
} catch (const std::exception& e) {
std::cout << "deferred: " << e.what() << '\n';
}
}
}
std::error_code/std::system_error
An alternative to exceptions for failures that are cheap and common enough that throwing would be too costly or too disruptive to control flow — an integer code plus a "category" that gives it meaning, rather than a string:
#include <system_error>
#include <iostream>
std::error_code readConfig() {
return std::make_error_code(std::errc::no_such_file_or_directory);
}
int main() {
if (auto ec = readConfig()) {
std::cout << ec.message() << " (" << ec.category().name() << ")\n";
}
try {
throw std::system_error(std::make_error_code(std::errc::permission_denied), "opening config");
} catch (const std::system_error& e) {
std::cout << e.what() << '\n';
}
}
<filesystem> (see Filesystem) is the standard library’s
biggest consumer of this pattern, via its error_code&-taking overloads.
std::expected as an Alternative
Covered fully in Vocabulary Types; broadly: exceptions suit
truly exceptional, rare failures where the caller usually can’t recover locally; std::expected (or
error_code) suits expected, common failure modes (a missing config key, invalid user input) that the
immediate caller is expected to handle, made visible directly in the function’s return type rather than hidden
in its exception specification.
assert
#include <cassert>
int divide(int a, int b) {
assert(b != 0 && "divisor must not be zero"); // checks an invariant -- a programmer error if it fails,
return a / b; // NOT a recoverable condition like the exceptions above
}
assert compiles to nothing when NDEBUG is defined (a typical "release" build) — never rely on an
`assert’s side effects, and never use it to validate untrusted external input (which must still fail
gracefully in release builds).
std::source_location
C++20’s std::source_location captures the calling file/line/function without the FILE/LINE
macro pair, and — as a default argument — automatically reflects the caller’s location, not the function’s
own:
#include <source_location>
#include <iostream>
void log(const std::string& message,
const std::source_location& loc = std::source_location::current()) {
std::cout << loc.file_name() << ':' << loc.line() << ": " << message << '\n';
}
int main() {
log("something happened"); // prints THIS call's file/line, not log()'s own
}
<stacktrace>
C++23’s std::stacktrace captures the current call stack without any platform-specific API:
#include <stacktrace>
#include <iostream>
void innerFunction() {
std::stacktrace trace = std::stacktrace::current();
std::cout << trace << '\n';
}
<stacktrace> compiles under this environment’s toolchain but additionally needs
-lstdc++_libbacktrace (GCC) linked in for std::stacktrace::current() to resolve at link time — a detail
worth knowing since the header alone compiling does not guarantee the program links without it.
|
std::terminate
The function called when error handling itself fails to handle an error — an exception escapes main, escapes
a noexcept function, or is thrown while another exception is already propagating (e.g. from a destructor
during unwinding). By default it calls std::abort(); std::set_terminate can install a custom handler (for
logging before exit), but that handler still cannot resume normal execution.
Exception Propagation and Stack Unwinding
(stack unwinding begins) funcC--xfuncB: exception propagates Note over funcB: local objects destroyed funcB--xfuncA: exception propagates Note over funcA: local objects destroyed funcA--xmain: exception propagates Note over main: caught here by a
matching catch clause
See Also
-
C: Error Handling and Program Failure — return codes,
errnoandgotocleanup — how the same problems are handled without exceptions or destructors.