C++ std::exception Hierarchy Explained

This article provides an overview of the standard exception hierarchy in C++, which is anchored by the base class std::exception. It examines the structural division between logic errors and runtime errors, outlines the specialized exceptions generated by language features and the standard library, and explains how to catch and extend these classes effectively in modern C++ applications.

The Root: std::exception

In C++, all standard library exceptions derive from the base class std::exception, defined in the <exception> header. This class provides an interface for error handling across the standard library. Its most critical member function is:

virtual const char* what() const noexcept;

This returns an implementation-defined, null-terminated explanatory string describing the cause of the failure. Polymorphic exception handling relies on catching const std::exception&, allowing applications to intercept any standard-derived error safely.

The Two Primary Branches: std::logic_error and std::runtime_error

Defined in the <stdexcept> header, the standard hierarchy branches into two distinct categories representing preventable programming defects versus unpredictable execution-state failures.

1. std::logic_error

std::logic_error represents faults in the internal logic of a program that, in theory, could be avoided by writing better code or enforcing proper preconditions.

The standard library derives several classes from std::logic_error:

  • std::invalid_argument: Thrown when an unacceptable argument is passed to a function (e.g., passing invalid format strings to std::stoi).
  • std::domain_error: Thrown when an input argument lies outside the mathematical domain for which an operation is defined.
  • std::length_error: Thrown when an attempt is made to exceed the maximum allowed size for an object (e.g., resizing a std::string or std::vector beyond its max_size()).
  • std::out_of_range: Thrown when attempting to access elements outside the valid index range, such as with std::vector::at or std::string::at.
  • std::future_error: Thrown by asynchronous operations in <future> when handling invalid promises or futures (derived from std::logic_error since C++11).

2. std::runtime_error

std::runtime_error represents events that cannot be easily predicted by static code analysis and depend on external factors, runtime state, or resource availability.

Common derivatives of std::runtime_error include:

  • std::range_error: Thrown when an internal computation produces a result outside the valid range.
  • std::overflow_error: Thrown when an arithmetic operation produces an overflow condition.
  • std::underflow_error: Thrown when a floating-point computation produces an underflow condition.
  • std::system_error: Introduced in C++11 to report operating system and low-level failures; it contains an std::error_code. Key derived classes include:
    • std::ios_base::failure: Thrown by stream I/O operations (derived from std::system_error since C++11).
    • std::filesystem::filesystem_error: Thrown by the filesystem library in C++17 on path or I/O failure.

Language and Resource Management Exceptions

Certain language expressions and core library components derive directly from std::exception rather than passing through std::logic_error or std::runtime_error.

Memory and Allocation

  • std::bad_alloc (<new>): Thrown by dynamic memory allocation operators (new) when system memory is exhausted.
    • std::bad_array_new_length: Derived from std::bad_alloc, thrown when allocating an array with a negative or excessive size.

Type Safety and RTTI

  • std::bad_cast (<typeinfo>): Thrown by dynamic_cast when a cast to a reference type fails.
  • std::bad_typeid (<typeinfo>): Thrown when applying the typeid operator to a null pointer of a polymorphic type.
  • std::bad_any_cast (<any>): Thrown by std::any_cast when converting to an incompatible target type.

Modern C++ Functional and Vocabulary Types

  • std::bad_function_call (<functional>): Thrown when invoking an empty std::function wrapper.
  • std::bad_weak_ptr (<memory>): Thrown when constructing a std::shared_ptr from an expired std::weak_ptr.
  • std::bad_optional_access (<optional>): Thrown when accessing the value of an empty std::optional via .value().
  • std::bad_variant_access (<variant>): Thrown when accessing the wrong alternative within an std::variant via std::get.

Execution Flow

  • std::bad_exception (<exception>): Used historically with dynamic exception specifications, and currently with exception handling mechanisms involving unexpected types.

Hierarchical Catching and Inheritance

Because the exception hierarchy relies on inheritance, catch blocks should always be arranged from the most derived type to the most base type to prevent base-class handlers from intercepting derived errors prematurely:

try {
    // Operations that may throw
} catch (const std::out_of_range& e) {
    // Handles specific boundary issues
} catch (const std::logic_error& e) {
    // Handles other logic defects
} catch (const std::runtime_error& e) {
    // Handles runtime execution issues
} catch (const std::exception& e) {
    // Fallback for all other standard exceptions
}

When authoring custom exceptions, derive from std::runtime_error or std::logic_error instead of std::exception directly, as their constructors natively accept custom string messages to initialize what().