Exceptions — throw, catch, Exception Hierarchy
C++ exceptions provide a structured mechanism to signal and handle error conditions that **cross function boundaries** — especially when returning an error code is inconvenient (constructors cannot return values). The exception mechanism: `throw` signals an error; the stack is unwound (destructors run — RAII guarantees resources are freed); `catch` handles the exception. The standard exception hierarchy (`std::exception` → `std::runtime_error`, `std::logic_error`, etc.) provides a common base for polymorphic catching.
Exception Hierarchy, throw by Value / catch by Reference & Custom Exceptions
#include <iostream>
#include <stdexcept>
#include <string>
#include <format> // C++20 (or use sstream as fallback)
// ── Standard exception hierarchy ─────────────────────────────
// std::exception (base: what())
// ├─ std::logic_error — bugs; precondition violations
// │ ├─ std::invalid_argument
// │ ├─ std::out_of_range
// │ └─ std::length_error
// └─ std::runtime_error — runtime failures
// ├─ std::overflow_error
// ├─ std::underflow_error
// └─ std::system_error — OS errors with error_code
// ── Custom exception — inherit from std ───────────────────────
class DatabaseError : public std::runtime_error {
int code_;
std::string table_;
public:
DatabaseError(int code, std::string table, std::string msg)
: std::runtime_error(msg)
, code_(code)
, table_(std::move(table))
{}
int code() const { return code_; }
const std::string& table() const { return table_; }
};
class ConnectionError : public DatabaseError {
public:
explicit ConnectionError(const std::string& host)
: DatabaseError(503, "", "Cannot connect to: " + host)
{}
};
// ── Throwing and catching ─────────────────────────────────────
void read_record(const std::string& table, int id) {
if (table.empty()) {
throw std::invalid_argument("Table name cannot be empty");
}
if (id <= 0) {
throw std::out_of_range("ID must be positive, got: " + std::to_string(id));
}
if (id > 1000) {
throw DatabaseError(404, table, "Record " + std::to_string(id) + " not found in " + table);
}
std::cout << "Read record " << id << " from " << table << "
";
}
void connect(const std::string& host) {
if (host.find("unreachable") != std::string::npos) {
throw ConnectionError(host);
}
std::cout << "Connected to " << host << "
";
}
void exception_demo() {
// ── ALWAYS catch by const reference — never by value ─────
// Catching by value: slices the exception (loses derived info)
// Catching by reference + const: no copy, polymorphic what()
try {
read_record("users", -1); // throws out_of_range
} catch (const std::out_of_range& e) {
std::cerr << "Out of range: " << e.what() << "
";
} catch (const std::exception& e) { // catch all std exceptions
std::cerr << "Exception: " << e.what() << "
";
}
try {
connect("db.unreachable.com");
} catch (const ConnectionError& e) {
std::cerr << "Connection failed: " << e.what() << " code=" << e.code() << "
";
} catch (const DatabaseError& e) {
std::cerr << "DB error: " << e.what() << " table=" << e.table() << "
";
}
try {
read_record("users", 9999); // throws DatabaseError
} catch (const DatabaseError& e) {
std::cerr << "DB: " << e.what() << " table=" << e.table() << "
";
}
// ── Catch-all and rethrow ─────────────────────────────────
try {
read_record("", 1);
} catch (...) { // catches ANYTHING
std::cerr << "Unknown exception — rethrowing
";
// throw; // rethrow same exception — preserves type
}
// ── Exception in constructor ──────────────────────────────
// Safe: partially constructed object's members are destructed
// Only fully constructed sub-objects get their destructors called
}
int main() {
exception_demo();
return 0;
}Common Mistakes
- Catching by value — `catch (std::exception e)` makes a copy and **slices** the exception. A `DatabaseError` caught as `std::exception` by value loses all the `code_` and `table_` information. Always catch by `const` reference.
- Catching too broad early — `catch (const std::exception&)` before `catch (const DatabaseError&)` will catch DatabaseError too, preventing the more specific handler from ever running. Catch most-derived classes first.
- Ignoring `std::terminate()` from exception in destructor — if a destructor throws during stack unwinding (another exception already active), `std::terminate()` is called immediately. Mark destructors `noexcept` and handle errors internally (log, set error flag).
Tip
Tip
Practice Exceptions throw catch Exception Hierarchy in small, isolated examples before integrating into larger projects. Breaking concepts into small experiments builds genuine understanding faster than reading alone.
The four pillars of Object-Oriented Programming in C++
Practice Task
Note
Practice Task — (1) Write a working example of Exceptions throw catch Exception Hierarchy from scratch without looking at notes. (2) Modify it to handle an edge case (empty input, null value, or error state). (3) Share your solution in the Priygop community for feedback.
Quick Quiz
Common Mistake
Warning
A common mistake with Exceptions throw catch Exception Hierarchy is skipping edge case testing — empty inputs, null values, and unexpected data types. Always validate boundary conditions to write robust, production-ready cpp code.
Key Takeaways
- C++ exceptions provide a structured mechanism to signal and handle error conditions that **cross function boundaries** — especially when returning an error code is inconvenient (constructors cannot return values).
- Catching by value — `catch (std::exception e)` makes a copy and **slices** the exception. A `DatabaseError` caught as `std::exception` by value loses all the `code_` and `table_` information. Always catch by `const` reference.
- Catching too broad early — `catch (const std::exception&)` before `catch (const DatabaseError&)` will catch DatabaseError too, preventing the more specific handler from ever running. Catch most-derived classes first.
- Ignoring `std::terminate()` from exception in destructor — if a destructor throws during stack unwinding (another exception already active), `std::terminate()` is called immediately. Mark destructors `noexcept` and handle errors internally (log, set error flag).