Lesson 17 · 17 of 20
Custom exception hierarchies · C++
Create your own C++ exceptions by deriving from std::runtime_error, with extra data, and catch them from the most specific to the most general.

Custom exceptions are built by inheriting from the standard ones. AppError derives from std::runtime_error and, with using std::runtime_error::runtime_error;, inherits its constructor, which takes the message that what() later returns. Every exception in the app is an AppError and therefore also a std::exception.
The derived classes carry useful information: ValidationError keeps the field that is wrong (field) and PaymentError the code the bank sent back (code). That way, whoever catches the error does not have to guess it from the message.
checkout validates the order and throws the matching exception: an email with no @, an empty cart or an amount the bank declines. If all is well, it charges and prints "OK". The loop goes through the orders with structured bindings ([email, amount]) and each try handles one, so a failure does not stop the rest.
catch clauses are tried in order and the first one that matches wins. That is why they go from most specific to most general: if catch (const AppError&) came first, it would also swallow the validation and payment errors. Always catch by `const` reference: it avoids copies and slicing, and keeps the real type.
Code · main.cpp
#include <iostream>#include <stdexcept>#include <string>#include <utility>#include <vector>// A family of errors: all of them are AppError, and AppError is a runtime_error.struct AppError : std::runtime_error { using std::runtime_error::runtime_error; };struct ValidationError : AppError { std::string field; // extra data about the error ValidationError(const std::string& f, const std::string& msg) : AppError(msg), field(f) {}};struct PaymentError : AppError { int code; explicit PaymentError(int c) : AppError("payment declined"), code(c) {}};void checkout(const std::string& email, double amount) { if (email.find('@') == std::string::npos) throw ValidationError("email", "missing @"); if (amount <= 0) throw AppError("the cart is empty"); if (amount > 1000) throw PaymentError(51); std::cout << "OK: " << amount << " EUR charged to " << email << "\n";}int main() { std::vector<std::pair<std::string, double>> orders = { {"ana@mail.com", 59.9}, {"luis.mail.com", 20}, {"eva@mail.com", 1500}, {"leo@mail.com", 0}}; for (const auto& [email, amount] : orders) { try { checkout(email, amount); } // the catches go from most to least specific catch (const ValidationError& e) { std::cout << "Invalid " << e.field << ": " << e.what() << "\n"; } catch (const PaymentError& e) { std::cout << "Bank code " << e.code << ": " << e.what() << "\n"; } catch (const AppError& e) { std::cout << "Cannot check out: " << e.what() << "\n"; } }}Output · c++ -std=c++17 main.cpp -o main && ./main
OK: 59.9 EUR charged to ana@mail.com Invalid email: missing @ Bank code 51: payment declined Cannot check out: the cart is empty
Key points
- Derive your exceptions from
std::runtime_error(or another standard one). using Base::Base;inherits the base constructors.- Exceptions can carry data besides the message.
- Order the
catchclauses from the most derived class to the most general. - Catch by
const&, never by value.
Exercise
Create StockError, derived from AppError, holding the name of the product that is out of stock. Throw it in checkout when the email is ana@mail.com and add its catch in the right place.