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

Exception Classes

The std::exception hierarchy, when to throw each standard type, how to write custom exceptions, and how to nest and transport exceptions across threads.

std::exceptionsince C++98

The polymorphic base class, declared in <exception>, of every exception the C++ standard library throws, exposing a virtual what() that returns an implementation-defined, null-terminated description.

Overview

The standard library defines a single-rooted hierarchy. Catching const std::exception& handles everything the library throws, but not arbitrary user types such as throw 42;, so a robust main still keeps a catch (...) fallback.

cpp
std::exception                                  // C++98
β”œβ”€β”€ logic_error                                 // <stdexcept>, C++98
β”‚   β”œβ”€β”€ invalid_argument, domain_error
β”‚   β”œβ”€β”€ length_error, out_of_range
β”‚   └── future_error                            // <future>, C++11
β”œβ”€β”€ runtime_error                               // <stdexcept>, C++98
β”‚   β”œβ”€β”€ range_error, overflow_error, underflow_error
β”‚   β”œβ”€β”€ system_error                            // <system_error>, C++11
β”‚   β”‚   └── ios_base::failure                   // derives from system_error since C++11
β”‚   β”œβ”€β”€ filesystem::filesystem_error            // C++17
β”‚   └── nonexistent_local_time, ambiguous_local_time // <chrono>, C++20
β”œβ”€β”€ bad_alloc                                   // <new>, C++98
β”‚   └── bad_array_new_length                    // C++11
β”œβ”€β”€ bad_cast, bad_typeid, bad_exception         // C++98
β”‚   └── bad_any_cast                            // <any>, C++17 (derives from bad_cast)
β”œβ”€β”€ bad_function_call, bad_weak_ptr             // C++11
β”œβ”€β”€ bad_optional_access, bad_variant_access     // C++17
β”œβ”€β”€ format_error                                // <format>, C++20 (derives from runtime_error)
└── bad_expected_access<E>                      // <expected>, C++23

format_error is in fact a runtime_error subtype, and bad_expected_access<E> derives from bad_expected_access<void>, which derives from std::exception. The tree above flattens these for readability.

The conventional split: logic_error means the caller violated a precondition that could have been checked beforehand (a bug). runtime_error means something went wrong that could not be predicted from the inputs alone (I/O, overflow in computed results, environment).

TypeTypical use
invalid_argumentArgument has the wrong value (std::stoi("abc") throws it)
out_of_rangeIndex or key outside valid range (vector::at, bitset::test, map::at, std::stod on overflow)
length_errorResult would exceed max_size()
domain_errorInput outside the mathematical domain of an operation
overflow_error / underflow_errorArithmetic result not representable
range_errorComputed result outside the representable range
bad_allocoperator new failure

Syntax

cpp
namespace std {
  class exception {                                     // <exception>
  public:
    exception() noexcept;
    exception(const exception&) noexcept;
    exception& operator=(const exception&) noexcept;
    virtual ~exception();                               // constexpr since C++26
    virtual const char* what() const noexcept;
  };

  class runtime_error : public exception {              // <stdexcept>
  public:
    explicit runtime_error(const std::string& what_arg);
    explicit runtime_error(const char* what_arg);       // C++11
    runtime_error(const runtime_error&) noexcept;       // copy must not throw
  };
}

logic_error, runtime_error, and their children store a copy of the message (typically reference-counted) so the copy constructor stays noexcept. Pre-C++11, what() was declared throw(); the meaning is the same. Dynamic exception specifications (throw(std::bad_alloc)) were deprecated in C++11, throw(T) forms were removed in C++17, and throw() was removed in C++20. Use noexcept (C++11).

Note that std::exception itself has no message constructor; throw std::exception(); yields an implementation-defined what(). Prefer a derived type that carries text.

Examples

Catching by const reference, ordered by specificity

cpp
#include <iostream>
#include <stdexcept>
#include <string>

double parse_ratio(const std::string& s) {
    try {
        return std::stod(s);                  // may throw invalid_argument / out_of_range
    } catch (const std::invalid_argument&) {
        throw;                                // rethrow the same object, no slicing
    }
}

int main() {
    try {
        std::cout << parse_ratio("1e999") << '\n';
    } catch (const std::out_of_range& e) {    // most specific first
        std::cerr << "range: " << e.what() << '\n';
    } catch (const std::exception& e) {
        std::cerr << "error: " << e.what() << '\n';
        return 1;
    } catch (...) {
        std::cerr << "unknown exception\n";
        return 2;
    }
}

Handlers are tried in order, so a base-class handler placed first shadows later derived ones. Always catch by const&; catching by value slices the object and loses the dynamic type.

A custom exception hierarchy

cpp
#include <stdexcept>
#include <string>
#include <system_error>   // std::error_code, C++11

class ConfigError : public std::runtime_error {
public:
    ConfigError(const std::string& key, const std::string& why)
        : std::runtime_error{"config '" + key + "': " + why}, key_{key} {}
    const std::string& key() const noexcept { return key_; }
private:
    std::string key_;
};

class IoError : public std::system_error {            // C++11
public:
    IoError(int errnum, const std::string& path)
        : std::system_error{errnum, std::generic_category(), path} {}
};

Derive from std::runtime_error or std::logic_error rather than std::exception directly: you inherit message storage and a noexcept copy. Build the message in the constructor; any extra std::string members can throw on copy, so keep payloads small or store them in a std::shared_ptr.

Nested exceptions (C++11)

cpp
#include <exception>
#include <iostream>
#include <stdexcept>

void load() { throw std::runtime_error{"file not found"}; }

void init() {
    try { load(); }
    catch (...) {
        std::throw_with_nested(std::logic_error{"init failed"});   // C++11
    }
}

void print(const std::exception& e, int depth = 0) {
    std::cerr << std::string(depth * 2, ' ') << e.what() << '\n';
    try {
        std::rethrow_if_nested(e);                                   // C++11
    } catch (const std::exception& inner) {
        print(inner, depth + 1);
    } catch (...) {}
}

int main() {
    try { init(); } catch (const std::exception& e) { print(e); }
}

throw_with_nested throws an unnamed type derived from both the argument's type and std::nested_exception, so existing catch (const std::logic_error&) handlers still match.

Transporting across threads (C++11)

cpp
#include <exception>
#include <thread>

std::exception_ptr g_err;

int main() {
    std::thread t{[] {
        try { throw std::runtime_error{"worker failed"}; }
        catch (...) { g_err = std::current_exception(); }
    }};
    t.join();
    if (g_err) std::rethrow_exception(g_err);
}

An exception escaping a std::thread function calls std::terminate, so capture it with current_exception. std::future and std::async do this for you.

Best Practices

  • Throw by value, catch by const&. The thrown object is copied or moved into exception storage; since C++17 guaranteed elision applies to the prvalue operand.
  • Make copy and move noexcept. If copying an exception throws during a throw, std::terminate is called.
  • Choose the category deliberately. Use logic_error descendants for contract violations and runtime_error descendants for environmental failures, so callers can handle the groups separately.
  • Use std::system_error with an error_code (C++11) when wrapping OS or library error codes, so callers can compare against portable std::errc conditions.
  • Mark destructors and move operations noexcept. Destructors are implicitly noexcept since C++11; throwing from one terminates.
  • Don't allocate heavily in exception construction paths if you must handle bad_alloc: the message string itself may fail to allocate.
  • Catch ... only at boundaries (thread entry points, C callbacks, main), where letting an exception propagate would be undefined or fatal.

Common Pitfalls

  • Slicing: catch (std::exception e) copies only the base subobject; what() then returns the base text, not your message.
  • Dangling what() pointers: the pointer is valid only while the exception object lives and until its what is next modified. Copy it to a std::string if you store it beyond the handler.
  • Relying on what() content: the text is implementation-defined for standard exceptions. Never parse it; switch on type or error_code.
  • Wrong order of handlers: catch (const std::exception&) before catch (const std::out_of_range&) makes the second unreachable (compilers usually warn).
  • bad_alloc is not recoverable by default: after memory exhaustion, handlers that allocate will fail again. Use new (std::nothrow) where a null result is the normal path.
  • Removed exception specifications: code using throw(Type) fails to compile in C++17 and later. std::unexpected and std::set_unexpected were removed in C++17 along with them, and std::bad_exception is now effectively vestigial.
  • Exceptions through noexcept: an exception leaving a noexcept function (C++11) calls std::terminate without unwinding guarantees.
  • Unconditional uncaught_exception(): it was deprecated in C++17 and removed in C++20. Use std::uncaught_exceptions() (C++17), which returns a count and works correctly in nested destructors, as in scope guards.
  • C++26: the standard exception types and their members are being made constexpr so exceptions can be thrown and caught in constant evaluation. Don't assume that on earlier standards or on compilers that haven't implemented it.

See Also

  • std::any, whose std::any_cast throws std::bad_any_cast (C++17)
  • std::optional::value() throws bad_optional_access (C++17); std::variant throws bad_variant_access (C++17); std::expected::value() throws bad_expected_access (C++23)
  • std::format throws format_error for invalid format strings (C++20); std::format with a literal is checked at compile time
  • <system_error>: std::error_code, std::error_category (C++11)
  • <exception>: exception_ptr, current_exception, rethrow_exception, make_exception_ptr, nested_exception (C++11)