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

sequenceDiagram participant main participant funcA participant funcB participant funcC main->>funcA: call funcA->>funcB: call funcB->>funcC: call funcC-->>funcC: throw std::runtime_error Note over funcC: local objects destroyed
(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