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++98The 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.
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++23format_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).
| Type | Typical use |
|---|---|
invalid_argument | Argument has the wrong value (std::stoi("abc") throws it) |
out_of_range | Index or key outside valid range (vector::at, bitset::test, map::at, std::stod on overflow) |
length_error | Result would exceed max_size() |
domain_error | Input outside the mathematical domain of an operation |
overflow_error / underflow_error | Arithmetic result not representable |
range_error | Computed result outside the representable range |
bad_alloc | operator new failure |
Syntax
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
#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
#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)
#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)
#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::terminateis called. - Choose the category deliberately. Use
logic_errordescendants for contract violations andruntime_errordescendants for environmental failures, so callers can handle the groups separately. - Use
std::system_errorwith anerror_code(C++11) when wrapping OS or library error codes, so callers can compare against portablestd::errcconditions. - Mark destructors and move operations
noexcept. Destructors are implicitlynoexceptsince 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 itswhatis next modified. Copy it to astd::stringif you store it beyond the handler. - Relying on
what()content: the text is implementation-defined for standard exceptions. Never parse it; switch on type orerror_code. - Wrong order of handlers:
catch (const std::exception&)beforecatch (const std::out_of_range&)makes the second unreachable (compilers usually warn). bad_allocis not recoverable by default: after memory exhaustion, handlers that allocate will fail again. Usenew (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::unexpectedandstd::set_unexpectedwere removed in C++17 along with them, andstd::bad_exceptionis now effectively vestigial. - Exceptions through
noexcept: an exception leaving anoexceptfunction (C++11) callsstd::terminatewithout unwinding guarantees. - Unconditional
uncaught_exception(): it was deprecated in C++17 and removed in C++20. Usestd::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
constexprso 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, whosestd::any_castthrowsstd::bad_any_cast(C++17)std::optional::value()throwsbad_optional_access(C++17);std::variantthrowsbad_variant_access(C++17);std::expected::value()throwsbad_expected_access(C++23)std::formatthrowsformat_errorfor invalid format strings (C++20);std::formatwith 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)