Why Inherit from std::exception in C++

In C++, developers have the freedom to throw almost any type, from primitive integers to dedicated structs. However, adhering to the standard convention of inheriting custom exception classes from std::exception or its child classes like std::runtime_error is vital for writing robust, maintainable, and predictable software. This article outlines the architectural, interoperability, and practical benefits of deriving your custom exceptions from the standard hierarchy.

1. Consistent Interface via what()

The base class std::exception defines the virtual method:

virtual const char* what() const noexcept;

By inheriting from std::exception (or using std::runtime_error), your custom exception guarantees this interface. This allows any logging framework, debugging utility, or generic error handler to retrieve a human-readable explanation of the failure without needing to know the exact derived type at compile time.

2. Polymorphic Handling and Unified Catch Blocks

Standard library functions throw exceptions that derive from std::exception (such as std::out_of_range or std::bad_alloc). When your custom types also inherit from this hierarchy, call sites can catch and handle both standard and custom failures uniformly:

try {
    performDatabaseOperation();
} catch (const std::exception& e) {
    std::cerr << "Operation failed: " << e.what() << '\n';
}

Catching by const std::exception& acts as a high-level safety net at architectural boundaries—such as thread roots or main()—ensuring that error descriptions are preserved.

3. Avoiding the Blindness of catch (...)

While catch (...) catches everything, it strips away all type and context information. When non-standard types are thrown (like raw strings, integers, or unrelated classes), the developer is forced to rely on catch (...) without access to an error message or backtrace information. Deriving from std::exception ensures errors can be intercepted safely with informative diagnostics.

4. Third-Party and Ecosystem Interoperability

Most modern C++ frameworks, test suites, and third-party libraries assume that thrown exceptions derive from std::exception. For example:

  • Testing frameworks (such as Google Test or Catch2) automatically print e.what() when an unexpected std::exception is caught during a test.
  • Web frameworks, GUI event loops, and game engines typically route unhandled standard exceptions to user-facing crash dialogs or telemetry services.

Throwing an isolated, non-conforming type often leads directly to std::terminate() because intermediate layers do not recognize it.

5. std::runtime_error Reduces Boilerplate

While std::exception requires you to manage and implement the what() method yourself, std::runtime_error provides a built-in constructor accepting a string message and manages its memory safely:

#include <stdexcept>
#include <string>

class DatabaseConnectionError : public std::runtime_error {
public:
    explicit DatabaseConnectionError(const std::string& msg)
        : std::runtime_error("Database failure: " + msg) {}
};

Choosing std::runtime_error simplifies custom exception implementation while maintaining full compatibility with standard exception handling semantics.