The standard exceptions (std::runtime_error, std::invalid_argument, and the rest) cover generic failures, but they can't tell a caller anything specific about a domain. When an order can't be processed because the cart is empty, or a checkout fails because three units were requested and only one is on the shelf, callers need a precise type they can catch and react to. Custom exception classes provide that precision: domain-specific error types that carry the context a caller needs.
A single generic exception type forces every caller to inspect message strings to figure out what went wrong. That's fragile, slow, and brittle to translation or refactoring. Suppose a checkout() function throws std::runtime_error("Cart is empty"). The handler ends up doing string comparison to identify the error:
That works, but the caller matches on message text. Change the wording and every caller breaks. Translate the message and every caller breaks. A second piece of code that also throws std::runtime_error for a different reason gets caught by the same handler.
The same logic with a dedicated type:
The caller catches EmptyCartException by type. The message is for humans reading logs; the type is for code making decisions. Two failures with the same wording but different types stay separate. Two failures with the same type but different wording stay together. Code that doesn't know about EmptyCartException falls through to the generic std::exception handler and gets a sensible default.
The other big win is selective catching. Domain-specific types let a caller handle only the failures it knows how to handle, and let everything else propagate up the call stack to a higher-level handler. With one big generic type, the choice is to swallow everything or write fragile string-matching logic.
The base class of every standard exception is std::exception from <exception>. It defines a single virtual function that matters here:
The default what() returns something like "std::exception", which is rarely useful. To make a custom exception meaningful, override what() and return a real message. The minimal form:
A few details deserve attention.
The function signature is exact: const char*, const, noexcept, and override. Each token matters.
const char* is a pointer to a C-style string. Whatever the function returns must stay valid for as long as the caller might use it.const means the function doesn't modify the exception object.noexcept means what() itself must not throw. Throwing while another exception is in flight is a disaster, so the standard prohibits it here.override is a C++11 keyword that tells the compiler "this overrides a virtual function from a base class". If the signature doesn't match the base's signature exactly, the result is a compile error instead of a new, unrelated function.Lessons and books written before C++11 often omit override. Don't. With override, a typo like what() vs What() is caught immediately. Without it, the typo creates a fresh function that never gets called, and what() falls back to the base class default.
The string literal "Cannot checkout: cart is empty" works because string literals have static storage duration. They live for the entire program, so returning a pointer to one is always safe. Returning a pointer to anything shorter-lived (a local buffer, a temporary std::string) creates a problem covered in the pitfalls section.
Writing the what() override and storing a message manually works, but it's repetitive. The standard library already has a helper for the boring parts: std::runtime_error (and its sibling std::logic_error) from <stdexcept>.
std::runtime_error has a constructor that takes a std::string, stores it internally, and returns its contents from what(). Inherit from it, forward a string to its constructor, and the job is done.
Three lines of class body, no manual what() override, no member string to manage. The constructor passes the message up to std::runtime_error, which copies it into its own internal buffer and serves it from what() for the life of the exception object.
Between the two parent classes:
| Base class | When to use |
|---|---|
std::exception | Minimal overhead, or full control over how the message is built and stored |
std::runtime_error | Pass a std::string message at construction and let the base handle storage |
std::logic_error | The error indicates a programming mistake (precondition violation, invalid argument), not an environmental failure |
For most domain exceptions, std::runtime_error is the right choice. It also has a virtual destructor inherited from std::exception, so no destructor declaration is needed.
Constructing a std::runtime_error copies the message string, which may allocate on the heap if the string is large. That's almost never the bottleneck (throws only happen on the unhappy path), but it's a real cost compared to returning a static const char*.
A message is a starting point, but real failures usually need more. When an OutOfStockException fires, the handler probably needs to know which product, how many were requested, and how many are actually available. Encode that in the exception's data members and expose them with getters.
The handler doesn't have to parse what() to recover the numbers. It pulls them straight out of the exception object using the getters. The message remains human-readable for logs, but the structured data is the real interface.
Two patterns are worth picking apart in this example:
The static `buildMessage` helper. std::runtime_error needs the message string passed to its constructor. The constructor can't run an arbitrary block of code, so the message has to come from a single expression. A static helper function that takes the same parameters and returns a std::string keeps the constructor readable. Using a static (not a member) function avoids the chicken-and-egg of calling a member function before any members are initialized.
Getters marked `const noexcept`. Getters never modify the object and never throw, so they get both qualifiers. The noexcept is especially important here because handlers often call getters while a wider exception flow is in progress, and a throwing getter would be just as bad as a throwing what().
Context doesn't have to be numeric. A PaymentDeclinedException might carry a std::string reason, an OrderNotFoundException carries an orderId, and a CustomerNotFoundException carries a customer email. The pattern is always the same: take the context as constructor parameters, store it in private members, expose it through const noexcept getters, and build a human-readable message for what() from the same data.
One custom exception is fine. The real benefit shows up with several related errors organized into a small hierarchy.
A typical pattern for an e-commerce backend: a single base exception for "anything that went wrong in the cart subsystem," and specific derived types for each kind of failure. Callers that handle every cart problem the same way catch the base. Callers that need to react differently to "empty" vs "out of stock" catch the leaves.
The structure is a tree with std::exception at the root, the domain base (CartException) one level down, and the leaf exceptions below that:
The leaf classes inherit from CartException, which itself inherits from std::runtime_error, which inherits from std::exception. Any handler that catches std::exception& catches everything below it in the tree. Any handler that catches CartException& catches all three leaves but ignores unrelated exceptions like std::bad_alloc. Any handler that catches EmptyCartException& catches only that one.
A reasonable handler chain at the top of an operation lines the catches up from most specific to most general:
The leaf catches handle their specific failures with the structured data those types expose. The CartException& catch picks up any future leaf added to the hierarchy without changing the calling code, which is the point of having the base in the first place. The final std::exception& catch is a safety net for unrelated failures like std::bad_alloc.
Order matters in the catch list. Put the most specific first and the most general last. The compiler usually warns about reversed order, but it isn't required to.
A few rules govern how to throw and catch custom exceptions. Most of them apply to all C++ exceptions, but they hit harder with derived types because the consequences of getting them wrong are more visible.
Throw by value, catch by reference. Always.
The throw is a temporary object. The exception system makes a copy that lives in its own storage for the duration of stack unwinding. The catch handler binds a reference to that storage.
What goes wrong with a by-value catch instead of by-reference? Object slicing. Consider this incorrect handler:
The message survives because it was stored in the base class's internal buffer. But the productId_ member is gone. The catch parameter e is a fresh CartException constructed by copying only the base-class slice of the thrown OutOfStockException. Anything specific to the derived type has been chopped off. A downcast of e back to OutOfStockException would fail.
Catch by reference and the slicing problem disappears, because a reference binds to the original object without copying:
const is the conventional default. The exception describes a failure; modifying the exception object is rarely useful and surprises readers. The exceptions to the const rule are rare (logging frameworks that augment the exception as it travels) and beyond the scope of this lesson.
Don't throw pointers. It's legal but exhausting: the catch site now owns the lifetime of the exception object and has to delete it. The standard library never throws pointers, and neither should application code.
Throw temporaries, not local variables. throw OutOfStockException(42, 5, 2); is one construction. Throwing a named local instead is a construction followed by a copy into the exception storage.
A custom base exception class (the CartException at the top of a hierarchy) needs a quick thought about its destructor.
std::exception has a virtual destructor. So does std::runtime_error. A base exception that inherits from one of those inherits a virtual destructor automatically, and no destructor declaration is needed. Derived destructors will be called correctly when an exception object is destroyed through a base reference or pointer.
A custom exception that does not inherit from a standard exception (not recommended, but legal) needs an explicitly virtual destructor:
Without that, deleting a derived object through a CustomBase* would have undefined behavior, and the destructors of the derived part would never run. For exception classes specifically, this matters less than for general inheritance hierarchies (exceptions are rarely deleted through base pointers; the runtime manages their lifetime), but it costs nothing and removes any chance of surprise.
The practical rule: inherit from `std::exception` or `std::runtime_error` for the base, and the destructor question takes care of itself.
Custom exceptions are simple, but a few mistakes keep showing up. The biggest one is returning a const char* from what() that doesn't outlive the exception.
What's wrong with this code?
This is undefined behavior. The local std::string msg is destroyed when what() returns. The const char* from msg.c_str() becomes dangling immediately. The caller dereferences it inside the catch block, reading freed memory. On some compilers and optimization settings, the correct message may print by accident, because the freed bytes haven't been overwritten yet. On other runs, on other compilers, or after a few small changes, the output is garbage or a crash.
The fix is to store the message in a member that lives as long as the exception object does, and return a pointer into that member.
Fix:
The message_ member is alive for the entire lifetime of the exception object. c_str() returns a pointer into that member's internal buffer, which stays valid until the catch block ends and the exception is destroyed.
This is the trick std::runtime_error performs internally, which is the main reason to inherit from it: skip the bookkeeping.
A few smaller pitfalls round out the list:
Forgetting `override`. Without it, a typo in the function name introduces a new function instead of overriding the base. Always write override on virtual overrides.
Forgetting `noexcept` on `what()`. A non-noexcept override is technically allowed (the base is noexcept, but C++ permits overriding a noexcept virtual with one that doesn't throw in practice). The catch is that any actual throw from what() calls std::terminate. Mark it noexcept so the compiler can enforce no-throw at the source level.
Catching by value to "be safe". Catching by value triggers object slicing, as covered above. Always catch by reference.
Throwing primitives like `throw 42;` or `throw "error";`. Legal, useless, and harmful. None of those types inherit from std::exception, so a generic catch (const std::exception&) won't catch them. Always throw an exception class.
9 quizzes