AlgoMaster Logo

Standard Exceptions (std::exception hierarchy)

Medium Priority21 min readUpdated June 6, 2026
Listen to this chapter
Unlock Audio

The C++ standard library ships with a small family of exception classes rooted at std::exception. Throwing one of these instead of a raw int or a hand-rolled string gives every catch site a single, predictable interface: a what() method that returns a description. The hierarchy also splits problems into two useful categories: bugs that could have been caught earlier, and failures that depend on the world outside the code.

The <exception> and <stdexcept> Headers

Two headers carry the bulk of the standard exception types. <exception> declares the base class std::exception plus a few low-level helpers and supplies the basic vocabulary. <stdexcept> declares the most useful concrete classes that the standard library and user code throw, like std::invalid_argument, std::out_of_range, and std::runtime_error.

A third set of exception types lives in other headers and gets pulled in indirectly. std::bad_alloc comes from <new>, std::bad_cast and std::bad_typeid come from <typeinfo>, std::bad_optional_access comes from <optional>, and std::bad_variant_access comes from <variant>. These headers are rarely included just for the exception. They arrive with the feature that throws them.

The catch handler accepts a const std::exception&, not a std::runtime_error&, yet the throw still matches. That works because std::runtime_error derives from std::exception. Polymorphism does the rest, and what() returns the message it was constructed with. This is the point of the hierarchy: one catch handler at the top of the tree can handle anything thrown from the standard library or any user-defined derivation.

The std::exception Base Class

std::exception is the common ancestor of every exception class in the standard library. It defines a tiny interface:

Only one method is interesting: what(). It returns a C-style string describing the error. It is marked noexcept, which means it promises not to throw, so it is safe to call inside a catch block without a second exception escaping during logging.

std::exception itself does not carry a message. Constructing one directly causes what() to return something unhelpful like "std::exception". The classes derived from it (std::runtime_error, std::logic_error, and related types) accept a message in their constructor and store it for what() to return later.

The lesson is simple: do not throw std::exception directly. Throw something that derives from it and carries a meaningful message.

The Full Hierarchy

The standard exception classes form a tree. At the top sits std::exception. Below it, two large branches split the world into logic errors (bugs in the code) and runtime errors (problems with the environment the code runs in). A handful of other classes sit directly under std::exception for special conditions like failed allocations and bad casts.

The cyan node at the top is the shared base. The orange branch holds logic errors, problems the programmer should have prevented. The teal branch holds runtime errors, problems caused by data, devices, or services outside the program. The green nodes are direct children of std::exception that do not fit neatly into either branch and tend to be thrown by specific language features.

Every class in this tree derives publicly from std::exception, so a single catch (const std::exception& e) handler will catch any of them. That is the practical payoff of the hierarchy: catch handlers can be as broad or as narrow as needed.

Logic Errors vs Runtime Errors

The split between std::logic_error and std::runtime_error is more than naming. It reflects two different kinds of problems, and confusing them is a common mistake when picking an exception type.

A logic error is something the programmer could have detected before the program ran. Passing a negative number to a function that requires a positive one. Asking for the 50th element of a 10-element vector. Calling a function in the wrong order. These are bugs in the code, not failures of the environment. In principle, the program could be proven never to make one of these calls by inspecting the source.

A runtime error depends on information the program only has at runtime. A network connection dropped. A file went missing between when it was opened and when it was read. A user typed an integer that overflows. A computation produced a number too large to represent. No amount of code review prevents these, because the error lives outside the program's control.

The distinction matters because it tells callers what kind of fix to expect. A logic error usually means "find the bug and fix the code." A runtime error usually means "handle this gracefully, retry, fall back, or show the user a friendly message."

TypeWhen to throwCaller's usual response
std::logic_error (and children)Programming mistake detectable from inputs aloneFix the bug in the caller
std::runtime_error (and children)Failure dependent on runtime stateRetry, fall back, or report to the user

Neither class is abstract. std::logic_error or std::runtime_error can be thrown directly with a message string, and the standard library does this in places. More often a specific subclass names the problem more precisely.

Common Logic Error Types

The four children of std::logic_error cover the most common kinds of programmer mistakes. Each carries a constructor that accepts const std::string& or const char*. Whatever is passed shows up in what().

std::invalid_argument

Throw this when a function receives an argument whose value does not make sense for the operation. The type might be fine; the value is not. The classic case is a function that expects a percentage between 0 and 100 receiving -25.

std::domain_error

A more specialized cousin of invalid_argument. Use it when an argument is outside the mathematical domain of a function. The square root of a negative number, the log of zero, the arccosine of 2. The standard library itself uses std::domain_error in <random> for certain distribution parameters.

std::length_error

Throw this when an operation would exceed a maximum allowed size. The classic example is std::vector::resize being asked to grow beyond max_size(), or std::string::reserve being asked for more characters than the string can hold. Most user code never throws length_error directly, but it occasionally appears from the standard library.

std::out_of_range

The main type in the logic-error branch. Throw it when an index or key falls outside the valid range of a container, an array, or any indexed structure. std::vector::at(), std::map::at(), and std::string::at() all throw std::out_of_range when given a bad index. The operator[] versions do not. They are undefined behavior on bad input. That is the difference between at() and [].

Common Runtime Error Types

The children of std::runtime_error cover failures that depend on runtime conditions. Like the logic-error subclasses, each takes a message in its constructor that what() will return.

std::range_error

Throw this when a computation produces a result outside the valid range for the target type. The naming is unfortunate because it sounds a lot like std::out_of_range, but the two are distinct. out_of_range is a logic error: someone passed a bad index. range_error is a runtime error: a calculation overshot what the result type can represent. The standard library uses range_error in <system_error> and in math conversions.

std::overflow_error

Throw this when an arithmetic operation overflows the destination type. The standard library throws it from <bitset> when converting a bitset to unsigned long and the value does not fit. Plain integer overflow on int and unsigned int does not throw automatically. C++ leaves that as undefined behavior for signed types and wraparound for unsigned. Overflow detection has to be written explicitly and thrown.

std::underflow_error

The mirror of overflow_error, used when a result is too small in magnitude to represent. Floating-point underflow in particular happens when a number is closer to zero than the smallest representable value. The standard library does not throw underflow_error in many places; it mostly exists so user code can signal the condition with a recognized type.

std::system_error

A specialized runtime error used when an operating system call fails. It carries an std::error_code alongside the message, providing both a readable description and a machine-checkable identifier. The standard library throws it from std::thread when thread creation fails, from filesystem operations in <filesystem>, and from some I/O code. It appears most often when working with concurrency or filesystem APIs.

When something does go wrong (out of memory for a thread stack, hitting the OS thread limit), the same code prints the error code and message.

Putting a runtime failure in context

Most everyday "this just failed" situations do not need one of the specialized subclasses. Plain std::runtime_error is fine, and it is what most production code throws when a request fails for environmental reasons.

Payment approval depends on a service that lives outside the program. There is no way to detect "the bank said no" by reading the source code, so runtime_error is the right family.

Other Standard Exceptions

A few exception classes derive directly from std::exception without going through logic_error or runtime_error. They live in different headers and get thrown by specific language features rather than by user code.

std::bad_alloc

Thrown by new (and other allocation paths) when the system cannot satisfy the request. It lives in <new>. Most C++ programs never see it, because modern operating systems over-commit memory and the program crashes from the OS before new ever fails. On embedded systems, in long-running services hitting their memory limit, or when allocating something obviously huge, bad_alloc is real.

bad_alloc::what() returns a fixed string. There is no constructor that takes a message because there is typically no memory left to allocate one.

std::bad_cast

Thrown by dynamic_cast when the cast is from a polymorphic reference and the conversion cannot succeed. It lives in <typeinfo>. The precondition matters: a pointer dynamic_cast failure returns nullptr instead of throwing. A reference dynamic_cast failure throws std::bad_cast, because a reference cannot be null.

std::bad_typeid

Thrown by typeid when applied to a dereferenced null polymorphic pointer. It lives in <typeinfo>. It is rare to encounter, because the only way to trigger it is to dereference a null pointer first, which is itself a bug.

std::bad_function_call

Thrown when an empty std::function is invoked. It lives in <functional>. An empty std::function is one that was default-constructed or reset, with no callable target inside.

std::bad_optional_access (C++17)

Thrown when .value() is called on an empty std::optional. It lives in <optional>. Calling .value() is the safe way to extract the contents (the unsafe way, *opt, is undefined behavior if the optional is empty).

std::bad_variant_access (C++17)

Thrown by std::get on a std::variant when the variant currently holds a different alternative than the one requested. It lives in <variant>.

Constructing Standard Exceptions

The two main families, std::logic_error and std::runtime_error, and all their children take a message in the constructor. Two overloads are provided, one for const std::string& and one for const char*. Either way, the bytes are copied into the exception object and survive as long as the exception lives. what() returns a pointer into that stored buffer.

The string-building style on the second throw is the everyday case. The message carries context (which customer, which order, which file) so the catch handler can log something useful. Do not go overboard: the exception is for the program to understand. Diagnostic detail belongs in logs.

The bad_* family (bad_alloc, bad_cast, bad_typeid, bad_function_call, bad_optional_access, bad_variant_access) does not take a message. Their what() returns a fixed string. These are not thrown directly from user code anyway. The language or library throws them.

A Common Mistake: Wrong Branch

A real example of using the wrong type. Consider a function that reads a config file and throws std::logic_error when the file is missing.

This compiles and runs, but the exception type is wrong. Whether the file exists has nothing to do with a bug in the source code. The file might be there during testing and missing in deployment. The condition only becomes knowable at runtime, when the open is attempted. That makes it a runtime error, not a logic error. Catch sites watching for std::runtime_error (the right home for I/O failures) will miss it. The fix is one word:

The rule of thumb: ask whether a careful code review could prove the condition never happens. If yes, it is a logic error. If the answer depends on something outside the program, it is a runtime error.

Choosing the Right Standard Exception

When using throw, pick the most specific class that fits the situation. A more specific type lets catch handlers match precisely and lets readers understand the failure at a glance. If nothing fits, fall back to the parent (std::logic_error or std::runtime_error) rather than inventing something that does not match.

ExceptionThrow when...Example
std::invalid_argumentAn argument value is unacceptable for the operationsetDiscount(-10.0)
std::domain_errorAn argument is outside the function's mathematical domainsqrt(-1.0)
std::length_errorAn operation would exceed a maximum sizevector::reserve(too_big)
std::out_of_rangeAn index or key is past the end of a containervec.at(50) on a 10-element vector
std::range_errorA computed result is outside the representable rangeA conversion produces a value too large
std::overflow_errorAn arithmetic operation overflowsA bitset value doesn't fit in unsigned long
std::underflow_errorA floating-point result is too small to representA multiplication underflows to zero
std::system_errorAn OS-level operation failedstd::thread cannot start a new thread
std::runtime_errorAny other runtime failurePayment declined, network unreachable, parse failed
std::logic_errorAny other programmer mistakeA function called in the wrong state
std::bad_alloc(Thrown by new automatically)Out of memory
std::bad_cast(Thrown by reference dynamic_cast on failure)Wrong derived type at runtime
std::bad_optional_access(Thrown by optional::value() when empty)Reading an absent value
std::bad_variant_access(Thrown by std::get on wrong alternative)Wrong type extracted from variant

A short decision flow that captures the choices:

The diagram is a quick reference, not a strict algorithm. The first decision (logic vs runtime) carries most of the weight. Once that call is made, the right subclass usually picks itself.

Catching Standard Exceptions

Since every standard exception derives from std::exception, catching them all with one handler at the top of the tree works, or narrower handlers can react differently to different failures. Either way, catch by const reference, not by value. Catching by value slices the exception and loses any extra information the derived class carries.

Three handlers, three different reactions, all selected by the type of the thrown object. The order matters: catch more specific types first, then more general ones. If std::exception is caught before std::out_of_range, the broader handler matches everything and the specific handler never fires.

Quiz

Standard Exceptions Quiz

10 quizzes