cpp-know-hows

cpp related stuff

View on GitHub

Exception Handling and Error Management

Overview

Exception handling is C++’s primary mechanism for dealing with runtime errors. This chapter covers exception mechanics, safety guarantees, modern error handling techniques, and the upcoming std::expected (C++23).

┌────────────────────────────────────────────────────────┐
│           EXCEPTION HANDLING ECOSYSTEM                 │
├────────────────────────────────────────────────────────┤
│                                                        │
│  EXCEPTIONS      │  ERROR CODES     │  MODERN          │
│  ──────────      │  ───────────     │  ──────          │
│  • try/catch     │  • error_code    │  • expected      │
│  • throw         │  • error_category│  • noexcept      │
│  • Standard exc  │  • system_error  │  • Contracts     │
│  • Custom exc    │                  │                  │
│                                                        │
│  SAFETY GUARANTEES                                     │
│  ─────────────────                                     │
│  • No-throw (noexcept)                                 │
│  • Strong (commit-or-rollback)                         │
│  • Basic (no leaks, valid state)                       │
│                                                        │
└────────────────────────────────────────────────────────┘

Exception Basics

try-catch-throw

#include <iostream>
#include <stdexcept>

double divide(double a, double b) {
    if (b == 0.0) {
        throw std::invalid_argument("Division by zero");
    }
    return a / b;
}

int main() {
    try {
        double result = divide(10.0, 0.0);
        std::cout << "Result: " << result << std::endl;
    }
    catch (const std::invalid_argument& e) {
        std::cerr << "Error: " << e.what() << std::endl;
    }
    catch (const std::exception& e) {
        std::cerr << "Unknown error: " << e.what() << std::endl;
    }
    catch (...) {
        std::cerr << "Unknown exception" << std::endl;
    }
    
    return 0;
}

Exception Flow

Normal execution:
main() → function_a() → function_b() → return → return → return

Exception thrown:
main() → function_a() → function_b() → throw
   ↑                                      │
   └──────────── unwinds stack ───────────┘
                 (catch or terminate)

Stack unwinding:
┌─────────────────────────────────────────────┐
│ function_c() throws exception               │
│ function_b() stack unwound, destructors run │
│ function_a() stack unwound, destructors run │
│ main() catches exception                    │
└─────────────────────────────────────────────┘

Standard Exceptions

Exception Hierarchy

#include <exception>
#include <stdexcept>

/*
Exception Hierarchy:

std::exception
├── std::bad_alloc
├── std::bad_cast
├── std::bad_typeid
├── std::bad_exception
├── std::bad_function_call (C++11)
├── std::bad_weak_ptr (C++11)
├── std::bad_optional_access (C++17)
├── std::bad_variant_access (C++17)
├── std::bad_any_cast (C++17)
└── std::logic_error
    ├── std::invalid_argument
    ├── std::domain_error
    ├── std::length_error
    ├── std::out_of_range
    └── std::future_error (C++11)
└── std::runtime_error
    ├── std::range_error
    ├── std::overflow_error
    ├── std::underflow_error
    └── std::system_error (C++11)
        ├── std::ios_base::failure
        └── std::filesystem::filesystem_error (C++17)
*/

void example_exceptions() {
    try {
        // Logic errors (programming bugs)
        throw std::invalid_argument("Invalid argument");
        throw std::out_of_range("Index out of range");
        throw std::length_error("Length exceeded");
        throw std::domain_error("Domain error");
        
        // Runtime errors (external conditions)
        throw std::runtime_error("Runtime error");
        throw std::overflow_error("Overflow");
        throw std::underflow_error("Underflow");
        
        // System errors
        throw std::system_error(errno, std::system_category());
        
    } catch (const std::exception& e) {
        std::cerr << e.what() << std::endl;
    }
}

Using Standard Exceptions

#include <stdexcept>
#include <vector>

class BankAccount {
    double balance_;
    
public:
    BankAccount(double initial) : balance_(initial) {
        if (initial < 0) {
            throw std::invalid_argument("Initial balance cannot be negative");
        }
    }
    
    void withdraw(double amount) {
        if (amount < 0) {
            throw std::invalid_argument("Amount cannot be negative");
        }
        if (amount > balance_) {
            throw std::runtime_error("Insufficient funds");
        }
        balance_ -= amount;
    }
    
    double get_balance() const { return balance_; }
};

void process_accounts(const std::vector<BankAccount>& accounts, size_t index) {
    if (index >= accounts.size()) {
        throw std::out_of_range("Account index out of range");
    }
    // Process account...
}

Custom Exceptions

Creating Custom Exception Classes

#include <exception>
#include <string>

// Simple custom exception
class MyException : public std::exception {
    std::string message_;
    
public:
    explicit MyException(const std::string& msg) : message_(msg) {}
    
    const char* what() const noexcept override {
        return message_.c_str();
    }
};

// Exception with error code
class DatabaseError : public std::runtime_error {
    int error_code_;
    
public:
    DatabaseError(const std::string& msg, int code)
        : std::runtime_error(msg), error_code_(code) {}
    
    int error_code() const { return error_code_; }
};

// Exception hierarchy for domain
class NetworkException : public std::runtime_error {
    using std::runtime_error::runtime_error;
};

class ConnectionError : public NetworkException {
    using NetworkException::NetworkException;
};

class TimeoutError : public NetworkException {
    using NetworkException::NetworkException;
};

// Usage
void connect_to_server() {
    throw ConnectionError("Failed to connect to server");
}

int main() {
    try {
        connect_to_server();
    }
    catch (const ConnectionError& e) {
        std::cerr << "Connection error: " << e.what() << std::endl;
    }
    catch (const NetworkException& e) {
        std::cerr << "Network error: " << e.what() << std::endl;
    }
    
    return 0;
}

Exception Safety Guarantees

Three Levels of Safety

┌────────────────────────────────────────────────────────┐
│          Exception Safety Guarantees                   │
├────────────────────────────────────────────────────────┤
│                                                        │
│ 1. NO-THROW GUARANTEE (noexcept)                      │
│    • Never throws exceptions                           │
│    • Required for: destructors, move operations, swap  │
│    • Example: int* ptr = nullptr;                      │
│                                                        │
│ 2. STRONG GUARANTEE (commit-or-rollback)              │
│    • Either completes successfully OR                  │
│    • Leaves state unchanged (as if not called)         │
│    • Example: vector::push_back (if realloc fails)     │
│                                                        │
│ 3. BASIC GUARANTEE                                     │
│    • No resource leaks                                 │
│    • Object remains in valid state                     │
│    • State may be changed but is consistent            │
│    • Example: most STL operations                      │
│                                                        │
│ 4. NO GUARANTEE (avoid!)                               │
│    • May leak resources                                │
│    • May leave object in invalid state                 │
│                                                        │
└────────────────────────────────────────────────────────┘

No-Throw Guarantee (noexcept)

#include <utility>

class Resource {
    int* data_;
    
public:
    // Destructor must be noexcept
    ~Resource() noexcept {
        delete data_;
    }
    
    // Move operations should be noexcept
    Resource(Resource&& other) noexcept
        : data_(other.data_) {
        other.data_ = nullptr;
    }
    
    Resource& operator=(Resource&& other) noexcept {
        if (this != &other) {
            delete data_;
            data_ = other.data_;
            other.data_ = nullptr;
        }
        return *this;
    }
    
    // Swap should be noexcept
    void swap(Resource& other) noexcept {
        std::swap(data_, other.data_);
    }
};

// Conditional noexcept
template<typename T>
class Container {
public:
    void push_back(T&& value) noexcept(noexcept(T(std::move(value)))) {
        // noexcept if T's move constructor is noexcept
    }
};

Strong Guarantee

#include <memory>
#include <utility>

class Widget {
    int value_;
    std::unique_ptr<int[]> data_;
    
public:
    // Strong guarantee using copy-and-swap
    void set_data(int value, size_t size) {
        // Create new data
        auto new_data = std::make_unique<int[]>(size);
        // May throw, but Widget unchanged
        
        for (size_t i = 0; i < size; ++i) {
            new_data[i] = value;
        }
        
        // No throw from here
        value_ = value;
        data_ = std::move(new_data);
        // Commit: success, or above threw and Widget unchanged
    }
    
    // Strong guarantee for assignment
    Widget& operator=(const Widget& other) {
        Widget temp(other);  // May throw, but *this unchanged
        swap(temp);          // No throw
        return *this;
    }
    
    void swap(Widget& other) noexcept {
        std::swap(value_, other.value_);
        std::swap(data_, other.data_);
    }
};

Basic Guarantee

class Logger {
    std::vector<std::string> logs_;
    
public:
    void add_log(const std::string& msg) {
        // Basic guarantee: if push_back throws,
        // logs_ is in valid state (unchanged)
        logs_.push_back(msg);
        
        // But state is changed (msg added)
        // No leak, object valid, but state modified
    }
};

noexcept Specification

Using noexcept

#include <type_traits>

// Always noexcept
void guaranteed_no_throw() noexcept {
    // Must not throw
}

// Conditional noexcept
template<typename T>
void conditional_no_throw(T&& value) 
    noexcept(std::is_nothrow_move_constructible_v<T>) {
    // noexcept depends on T
}

// Check if noexcept
void check_noexcept() {
    static_assert(noexcept(guaranteed_no_throw()));
    
    constexpr bool is_noexcept = noexcept(std::declval<int>() + 1);
}

// Destructors are implicitly noexcept
class MyClass {
public:
    ~MyClass() {  // implicitly noexcept
        // Don't throw in destructor!
    }
};

// Override if really needed (dangerous!)
class DangerousClass {
public:
    ~DangerousClass() noexcept(false) {
        // Can throw, but usually bad idea
    }
};

noexcept and Performance

#include <vector>

class MoveOnly {
public:
    MoveOnly() = default;
    MoveOnly(MoveOnly&&) noexcept = default;  // Important!
    MoveOnly& operator=(MoveOnly&&) noexcept = default;
    
    // If move is noexcept, vector can use move during reallocation
    // Otherwise, vector must copy (for strong guarantee)
};

void performance_example() {
    std::vector<MoveOnly> vec;
    
    // If MoveOnly's move is noexcept:
    //   - vector uses move during reallocation → fast
    // If move might throw:
    //   - vector uses copy during reallocation → slow
    
    vec.push_back(MoveOnly{});
}

Error Codes (Alternative to Exceptions)

std::error_code

#include <system_error>
#include <iostream>

// Define custom error enum
enum class MyError {
    Success = 0,
    NotFound,
    PermissionDenied,
    InvalidInput
};

// Create error category
class MyErrorCategory : public std::error_category {
public:
    const char* name() const noexcept override {
        return "MyError";
    }
    
    std::string message(int ev) const override {
        switch (static_cast<MyError>(ev)) {
            case MyError::Success: return "Success";
            case MyError::NotFound: return "Not found";
            case MyError::PermissionDenied: return "Permission denied";
            case MyError::InvalidInput: return "Invalid input";
            default: return "Unknown error";
        }
    }
};

const MyErrorCategory& my_error_category() {
    static MyErrorCategory instance;
    return instance;
}

std::error_code make_error_code(MyError e) {
    return {static_cast<int>(e), my_error_category()};
}

// Must specialize is_error_code_enum
namespace std {
    template<>
    struct is_error_code_enum<MyError> : true_type {};
}

// Usage
std::error_code do_something() {
    // ...
    if (/* error condition */) {
        return MyError::NotFound;
    }
    return MyError::Success;
}

int main() {
    auto ec = do_something();
    
    if (ec) {  // Check if error
        std::cerr << "Error: " << ec.message() << std::endl;
    }
    
    if (ec == MyError::NotFound) {
        // Handle specific error
    }
    
    return 0;
}

std::system_error

#include <system_error>
#include <fstream>

void read_file(const char* filename) {
    std::ifstream file(filename);
    
    if (!file) {
        throw std::system_error(
            errno,
            std::system_category(),
            "Failed to open file"
        );
    }
    
    // Read file...
}

int main() {
    try {
        read_file("nonexistent.txt");
    }
    catch (const std::system_error& e) {
        std::cerr << "System error: " << e.what() << std::endl;
        std::cerr << "Error code: " << e.code() << std::endl;
        std::cerr << "Error value: " << e.code().value() << std::endl;
    }
    
    return 0;
}

std::expected (C++23)

Result Type Pattern

#include <expected>  // C++23
#include <string>

// Return value or error
std::expected<int, std::string> divide(int a, int b) {
    if (b == 0) {
        return std::unexpected("Division by zero");
    }
    return a / b;
}

int main() {
    auto result = divide(10, 2);
    
    if (result) {  // Has value
        std::cout << "Result: " << *result << std::endl;
        // Or: result.value()
    } else {  // Has error
        std::cerr << "Error: " << result.error() << std::endl;
    }
    
    // With default value
    int value = result.value_or(0);
    
    // Transform operations
    auto result2 = divide(10, 2)
        .transform([](int x) { return x * 2; })
        .or_else([](auto&& error) {
            std::cerr << error << std::endl;
            return std::expected<int, std::string>(0);
        });
    
    return 0;
}

Comparison: Exceptions vs Error Codes vs Expected

┌────────────────────────────────────────────────────────┐
│     Error Handling Comparison                          │
├────────────────────────────────────────────────────────┤
│              │ Exceptions │ Error Codes │ expected    │
├──────────────┼────────────┼─────────────┼─────────────┤
│ Ergonomics   │ Good       │ Poor        │ Good        │
│ Performance  │ Slow path  │ Fast        │ Fast        │
│ Composability│ Good       │ Poor        │ Excellent   │
│ Type safety  │ Partial    │ Weak        │ Strong      │
│ Zero overhead│ No         │ Yes         │ Almost      │
│                                                        │
│ Use exceptions for: Rare, exceptional conditions       │
│ Use error codes for: C interop, performance critical   │
│ Use expected for: Regular errors, functional style     │
└────────────────────────────────────────────────────────┘

RAII and Exception Safety

Resource Management

#include <memory>
#include <fstream>
#include <mutex>

// BAD: Manual resource management
void bad_example() {
    int* data = new int[100];
    std::mutex mtx;
    mtx.lock();
    
    // If exception thrown here, leak!
    process_data(data);
    
    delete[] data;
    mtx.unlock();
}

// GOOD: RAII handles cleanup
void good_example() {
    auto data = std::make_unique<int[]>(100);
    std::lock_guard<std::mutex> lock(mtx);
    
    // If exception thrown, automatic cleanup
    process_data(data.get());
    
    // No manual cleanup needed
}

// Custom RAII wrapper
class FileHandle {
    FILE* file_;
public:
    explicit FileHandle(const char* name, const char* mode)
        : file_(fopen(name, mode)) {
        if (!file_) throw std::runtime_error("Failed to open");
    }
    
    ~FileHandle() { if (file_) fclose(file_); }
    
    // Delete copy, allow move
    FileHandle(const FileHandle&) = delete;
    FileHandle& operator=(const FileHandle&) = delete;
    FileHandle(FileHandle&& other) noexcept : file_(other.file_) {
        other.file_ = nullptr;
    }
    
    FILE* get() const { return file_; }
};

Best Practices

1. Never Throw from Destructors

class Bad {
public:
    ~Bad() {
        throw std::runtime_error("Bad!");  // NEVER DO THIS!
    }
};

class Good {
public:
    ~Good() noexcept {  // Explicitly noexcept
        try {
            cleanup();  // If this might throw
        } catch (...) {
            // Log error, but don't let it escape
        }
    }
};

2. Catch by const Reference

try {
    // ...
} catch (const std::exception& e) {  // GOOD: const ref
    std::cerr << e.what();
}

// BAD: Catches by value (slicing!)
catch (std::exception e) {  // BAD
    // Derived class info lost
}

// OK for rethrow
catch (...) {
    log_error();
    throw;  // Rethrow same exception
}

3. Order Exception Handlers

try {
    // ...
}
catch (const std::out_of_range& e) {  // Most specific first
    // ...
}
catch (const std::logic_error& e) {   // Less specific
    // ...
}
catch (const std::exception& e) {      // Most general
    // ...
}
catch (...) {                          // Catch-all last
    // ...
}

4. Document Exception Specifications

/**
 * @brief Divides two numbers
 * @throws std::invalid_argument if divisor is zero
 * @throws std::overflow_error if result overflows
 */
double divide(double a, double b);

5. Use RAII for All Resources

// Every resource should have an owning RAII wrapper
std::unique_ptr<T>     // for pointers
std::lock_guard        // for mutexes
std::fstream           // for files
std::vector            // for arrays
// etc.

Performance Considerations

Zero-Overhead Principle

// If no exception thrown, exception handling has zero overhead
// Exception path is slow, but normal path is optimized

void hot_path() noexcept {
    // Critical performance code
    // noexcept allows better optimization
}

void cold_path() {
    // Can throw exceptions
    // Less performance critical
}

Avoid Exceptions in Performance-Critical Code

// HOT PATH: Use error codes or expected
std::error_code process_packet(Packet& p);

// COLD PATH: Exceptions OK
void load_config(const char* filename);  // May throw

Common Patterns

Function Try Blocks

class Widget {
    Resource r_;
    
public:
    Widget()
    try : r_(initialize()) {
        // Constructor body
    }
    catch (const std::exception& e) {
        // Handle initialization exception
        // Automatically rethrows after handler
    }
};

Exception Translator

void c_api_function() {
    try {
        cpp_function();
    }
    catch (const std::exception& e) {
        // Translate C++ exception to C error code
        set_last_error(translate_exception(e));
    }
    catch (...) {
        set_last_error(UNKNOWN_ERROR);
    }
}

Complete Practical Example: Robust Database Connection Manager

Here’s a comprehensive example integrating exception handling, RAII, error codes, and all safety guarantees:

#include <iostream>
#include <exception>
#include <stdexcept>
#include <string>
#include <memory>
#include <vector>
#include <optional>
#include <system_error>
#include <chrono>

// Custom exception hierarchy
class DatabaseException : public std::runtime_error {
public:
    using std::runtime_error::runtime_error;
};

class ConnectionException : public DatabaseException {
public:
    explicit ConnectionException(const std::string& msg) 
        : DatabaseException("Connection error: " + msg) {}
};

class QueryException : public DatabaseException {
    int error_code_;
public:
    QueryException(const std::string& msg, int code)
        : DatabaseException("Query error: " + msg)
        , error_code_(code) {}
    
    int error_code() const { return error_code_; }
};

class TransactionException : public DatabaseException {
public:
    using DatabaseException::DatabaseException;
};

// Error code enum for non-exception path
enum class DBError {
    Success = 0,
    ConnectionFailed,
    QueryFailed,
    TransactionFailed,
    Timeout,
    PermissionDenied
};

// RAII Connection wrapper (no-throw guarantee for destructor)
class DBConnection {
private:
    bool connected_ = false;
    std::string connection_string_;
    
    // Simulate connection
    void connect() {
        std::cout << "  [Connecting to: " << connection_string_ << "]\n";
        
        // Simulate connection that might fail
        if (connection_string_.find("invalid") != std::string::npos) {
            throw ConnectionException("Invalid connection string");
        }
        
        connected_ = true;
        std::cout << "  [Connected successfully]\n";
    }
    
public:
    explicit DBConnection(const std::string& conn_str)
        : connection_string_(conn_str) {
        connect();
    }
    
    // Destructor must be noexcept
    ~DBConnection() noexcept {
        try {
            if (connected_) {
                std::cout << "  [Disconnecting...]\n";
                connected_ = false;
            }
        } catch (...) {
            // Never let exceptions escape destructor
            std::cerr << "  [ERROR: Exception in destructor caught]\n";
        }
    }
    
    // Delete copy (connection is unique resource)
    DBConnection(const DBConnection&) = delete;
    DBConnection& operator=(const DBConnection&) = delete;
    
    // Move operations should be noexcept
    DBConnection(DBConnection&& other) noexcept
        : connected_(other.connected_)
        , connection_string_(std::move(other.connection_string_)) {
        other.connected_ = false;
    }
    
    DBConnection& operator=(DBConnection&& other) noexcept {
        if (this != &other) {
            connected_ = other.connected_;
            connection_string_ = std::move(other.connection_string_);
            other.connected_ = false;
        }
        return *this;
    }
    
    bool is_connected() const noexcept { return connected_; }
    
    // Query execution with strong exception guarantee
    std::vector<std::string> execute_query(const std::string& query) {
        if (!connected_) {
            throw DatabaseException("Not connected");
        }
        
        std::cout << "  [Executing: " << query << "]\n";
        
        // Simulate query that might fail
        if (query.find("error") != std::string::npos) {
            throw QueryException("Syntax error in query", 1064);
        }
        
        // Return dummy results
        return {"row1", "row2", "row3"};
    }
};

// Transaction with RAII and strong exception guarantee
class Transaction {
private:
    DBConnection& conn_;
    bool committed_ = false;
    bool rolled_back_ = false;
    
public:
    explicit Transaction(DBConnection& conn) : conn_(conn) {
        std::cout << "  [BEGIN TRANSACTION]\n";
    }
    
    ~Transaction() noexcept {
        try {
            if (!committed_ && !rolled_back_) {
                std::cout << "  [AUTO-ROLLBACK (transaction not committed)]\n";
                rollback();
            }
        } catch (...) {
            // Swallow exception in destructor
        }
    }
    
    void commit() {
        if (rolled_back_) {
            throw TransactionException("Cannot commit rolled-back transaction");
        }
        
        std::cout << "  [COMMIT]\n";
        committed_ = true;
    }
    
    void rollback() noexcept {
        std::cout << "  [ROLLBACK]\n";
        rolled_back_ = true;
    }
};

// Connection pool with exception safety
class ConnectionPool {
private:
    std::vector<std::unique_ptr<DBConnection>> connections_;
    std::string connection_string_;
    size_t max_size_;
    
public:
    ConnectionPool(std::string conn_str, size_t max_size)
        : connection_string_(std::move(conn_str))
        , max_size_(max_size) {
        
        connections_.reserve(max_size_);  // Reserve for exception safety
    }
    
    // Strong exception guarantee: either adds connection or leaves state unchanged
    void add_connection() {
        if (connections_.size() >= max_size_) {
            throw std::length_error("Pool is full");
        }
        
        // Create new connection (might throw)
        auto conn = std::make_unique<DBConnection>(connection_string_);
        
        // If we get here, connection succeeded
        // Now commit the change (no-throw)
        connections_.push_back(std::move(conn));
        
        std::cout << "  [Pool size: " << connections_.size() << "]\n";
    }
    
    size_t size() const noexcept { return connections_.size(); }
};

// Error handling without exceptions (error codes)
class ErrorCodeDB {
public:
    struct Result {
        DBError error;
        std::vector<std::string> data;
        
        explicit operator bool() const { return error == DBError::Success; }
    };
    
    static Result query_no_throw(const std::string& query) noexcept {
        Result result{DBError::Success, { } };
        
        try {
            // Simulate query
            if (query.find("error") != std::string::npos) {
                result.error = DBError::QueryFailed;
                return result;
            }
            
            result.data = {"row1", "row2"};
        } catch (...) {
            result.error = DBError::QueryFailed;
        }
        
        return result;
    }
};

// Function-try block example
class DatabaseManager {
    DBConnection conn_;
    
public:
    // Constructor with function-try block
    DatabaseManager(const std::string& conn_str)
    try : conn_(conn_str) {
        std::cout << "  [DatabaseManager initialized]\n";
    }
    catch (const ConnectionException& e) {
        std::cerr << "  [Failed to initialize: " << e.what() << "]\n";
        throw;  // Automatically rethrows
    }
};

// Demonstration functions
void demo_basic_exception_handling() {
    std::cout << "\n=== Basic Exception Handling ===\n";
    
    try {
        DBConnection conn("localhost:5432");
        auto results = conn.execute_query("SELECT * FROM users");
        
        std::cout << "  Results: " << results.size() << " rows\n";
    }
    catch (const QueryException& e) {
        std::cerr << "  Query failed: " << e.what() 
                  << " (code: " << e.error_code() << ")\n";
    }
    catch (const ConnectionException& e) {
        std::cerr << "  Connection failed: " << e.what() << "\n";
    }
    catch (const DatabaseException& e) {
        std::cerr << "  Database error: " << e.what() << "\n";
    }
    catch (const std::exception& e) {
        std::cerr << "  Unexpected error: " << e.what() << "\n";
    }
}

void demo_raii_cleanup() {
    std::cout << "\n=== RAII Automatic Cleanup ===\n";
    
    try {
        DBConnection conn("localhost:5432");
        
        // Simulate error
        throw std::runtime_error("Something went wrong");
        
        // Connection automatically closed in destructor
    }
    catch (const std::exception& e) {
        std::cerr << "  Caught: " << e.what() << "\n";
    }
    // Connection destroyed and closed here
}

void demo_transaction_safety() {
    std::cout << "\n=== Transaction Safety ===\n";
    
    try {
        DBConnection conn("localhost:5432");
        Transaction txn(conn);
        
        conn.execute_query("INSERT INTO users VALUES (1, 'Alice')");
        conn.execute_query("INSERT INTO users VALUES (2, 'Bob')");
        
        // Simulate error before commit
        throw std::runtime_error("Error during transaction");
        
        txn.commit();  // Never reached
    }
    catch (const std::exception& e) {
        std::cerr << "  Transaction failed: " << e.what() << "\n";
        std::cerr << "  Transaction will auto-rollback\n";
    }
}

void demo_strong_guarantee() {
    std::cout << "\n=== Strong Exception Guarantee ===\n";
    
    try {
        ConnectionPool pool("localhost:5432", 3);
        
        pool.add_connection();  // OK
        pool.add_connection();  // OK
        
        std::cout << "  Pool has " << pool.size() << " connections\n";
        
        // Try to add invalid connection
        ConnectionPool bad_pool("invalid_server", 3);
        bad_pool.add_connection();  // Throws
        
        // This line never reached
        std::cout << "  This won't print\n";
    }
    catch (const std::exception& e) {
        std::cerr << "  Failed to add connection: " << e.what() << "\n";
        std::cerr << "  Pool state remains valid\n";
    }
}

void demo_error_codes() {
    std::cout << "\n=== Error Codes (No Exceptions) ===\n";
    
    auto result1 = ErrorCodeDB::query_no_throw("SELECT * FROM users");
    if (result1) {
        std::cout << "  Query succeeded: " << result1.data.size() << " rows\n";
    } else {
        std::cerr << "  Query failed with code: " 
                  << static_cast<int>(result1.error) << "\n";
    }
    
    auto result2 = ErrorCodeDB::query_no_throw("SELECT error FROM users");
    if (result2) {
        std::cout << "  Query succeeded\n";
    } else {
        std::cerr << "  Query failed with code: " 
                  << static_cast<int>(result2.error) << "\n";
    }
}

void demo_exception_hierarchy() {
    std::cout << "\n=== Exception Hierarchy ===\n";
    
    auto handle_db_operation = []() {
        throw QueryException("Table not found", 1146);
    };
    
    try {
        handle_db_operation();
    }
    catch (const QueryException& e) {
        std::cerr << "  Caught QueryException: " << e.what() 
                  << " (code: " << e.error_code() << ")\n";
    }
    catch (const DatabaseException& e) {
        // Would catch if QueryException wasn't caught above
        std::cerr << "  Caught DatabaseException: " << e.what() << "\n";
    }
    catch (const std::runtime_error& e) {
        // Would catch if DatabaseException wasn't caught above
        std::cerr << "  Caught runtime_error: " << e.what() << "\n";
    }
}

void demo_nested_exceptions() {
    std::cout << "\n=== Nested Exceptions ===\n";
    
    try {
        try {
            throw std::runtime_error("Original error");
        }
        catch (...) {
            std::throw_with_nested(DatabaseException("Wrapped error"));
        }
    }
    catch (const DatabaseException& e) {
        std::cerr << "  Outer exception: " << e.what() << "\n";
        
        try {
            std::rethrow_if_nested(e);
        }
        catch (const std::exception& nested) {
            std::cerr << "  Nested exception: " << nested.what() << "\n";
        }
    }
}

int main() {
    std::cout << "=== Exception Handling Demo ===\n";
    
    // 1. Basic exception handling
    demo_basic_exception_handling();
    
    // 2. RAII automatic cleanup
    demo_raii_cleanup();
    
    // 3. Transaction safety
    demo_transaction_safety();
    
    // 4. Strong exception guarantee
    demo_strong_guarantee();
    
    // 5. Error codes (no exceptions)
    demo_error_codes();
    
    // 6. Exception hierarchy
    demo_exception_hierarchy();
    
    // 7. Nested exceptions
    demo_nested_exceptions();
    
    std::cout << "\n=== Demo Complete ===\n";
    
    return 0;
}

Output (Sample):

=== Exception Handling Demo ===

=== Basic Exception Handling ===
  [Connecting to: localhost:5432]
  [Connected successfully]
  [Executing: SELECT * FROM users]
  Results: 3 rows
  [Disconnecting...]

=== RAII Automatic Cleanup ===
  [Connecting to: localhost:5432]
  [Connected successfully]
  Caught: Something went wrong
  [Disconnecting...]

=== Transaction Safety ===
  [Connecting to: localhost:5432]
  [Connected successfully]
  [BEGIN TRANSACTION]
  [Executing: INSERT INTO users VALUES (1, 'Alice')]
  [Executing: INSERT INTO users VALUES (2, 'Bob')]
  Transaction failed: Error during transaction
  Transaction will auto-rollback
  [AUTO-ROLLBACK (transaction not committed)]
  [ROLLBACK]
  [Disconnecting...]

=== Strong Exception Guarantee ===
  [Connecting to: localhost:5432]
  [Connected successfully]
  [Pool size: 1]
  [Connecting to: localhost:5432]
  [Connected successfully]
  [Pool size: 2]
  Pool has 2 connections
  [Connecting to: invalid_server]
  Failed to add connection: Connection error: Invalid connection string
  Pool state remains valid
  [Disconnecting...]
  [Disconnecting...]

Concepts Demonstrated:

This example shows robust, production-ready exception handling!


Next Steps


Part 17 of 22 - Exception Handling and Error Management