C++ Exception Handling: try, catch, throw, Custom Exceptions & Safety Complete Masterclass

โšก Modern C++ (C++17 / C++20 / C++23) ๐ŸŸข Lesson 18 ๐Ÿ“‚ Phase 18: Exception Handling ๐Ÿ“… 2026 Master Edition
๐Ÿ“Œ Covered in this in-depth guide: try/catch/throw ยท Re-throw ยท Multiple Catch Blocks ยท std::exception Hierarchy ยท bad_alloc/bad_cast ยท Custom Exception Hierarchy ยท No-throw/Strong/Basic Guarantees ยท noexcept ยท RAII Safety ยท copy-and-swap ยท exception_ptr

Welcome to Phase 18 (Chapter 18): C++ Exception Handling Masterclass! Exceptions provide a structured, type-safe mechanism for error propagation. Unlike C-style error codes that can be silently ignored, exceptions are impossible to ignore โ€” the program terminates if an exception isn't caught. Combined with RAII, exceptions enable leak-free, robust error handling across entire call stacks.

1try, catch, throw โ€” Complete Syntax
C++ โ€” try, catch, throw, re-throwโ–ถ Run in Compiler
#include <iostream>
#include <stdexcept>
#include <string>

// Function that throws
double safeDivide(double a, double b) {
    if (b == 0.0)
        throw std::invalid_argument("Division by zero is undefined!");
    if (a < 0 || b < 0)
        throw std::domain_error("Negative operands not allowed here");
    return a / b;
}

// Re-throwing example
void processValue(double x) {
    try {
        auto result = safeDivide(100.0, x);
        std::cout << "100 / " << x << " = " << result << "
";
    } catch (const std::invalid_argument& e) {
        std::cout << "[processValue] caught invalid_argument, re-throwing...
";
        throw;  // re-throw same exception (preserves original exception)
    }
}

int main() {
    // Basic try-catch
    try {
        std::cout << safeDivide(10.0, 2.0) << "
";   // OK
        std::cout << safeDivide(10.0, 0.0) << "
";   // throws!
    } catch (const std::invalid_argument& e) {
        std::cout << "Caught invalid_argument: " << e.what() << "
";
    }

    // Multiple catch blocks (most specific first!)
    try {
        throw std::out_of_range("index 99 is out of range [0,10]");
    } catch (const std::out_of_range& e) {     // most specific
        std::cout << "out_of_range: " << e.what() << "
";
    } catch (const std::logic_error& e) {       // base of out_of_range
        std::cout << "logic_error: " << e.what() << "
";
    } catch (const std::exception& e) {         // base of all std exceptions
        std::cout << "exception: " << e.what() << "
";
    } catch (...) {                              // catch ANYTHING
        std::cout << "Unknown exception!
";
    }

    // Re-throw chain
    try {
        processValue(0.0);
    } catch (const std::exception& e) {
        std::cout << "Top-level caught: " << e.what() << "
";
    }

    // Throw any type (int, string, custom struct)
    try {
        throw 42;           // throw an int
    } catch (int code) {
        std::cout << "Caught int code: " << code << "
";
    }
    return 0;
}
2Standard Exception Hierarchy
std::exception hierarchy: std::exception (base โ€” has .what()) โ”œโ”€โ”€ std::logic_error (programming errors โ€” detectable before runtime) โ”‚ โ”œโ”€โ”€ std::invalid_argument (bad argument value) โ”‚ โ”œโ”€โ”€ std::domain_error (mathematical domain violation) โ”‚ โ”œโ”€โ”€ std::length_error (exceeds max size) โ”‚ โ””โ”€โ”€ std::out_of_range (index/value out of range) โ””โ”€โ”€ std::runtime_error (errors only detectable at runtime) โ”œโ”€โ”€ std::range_error (computed value out of range) โ”œโ”€โ”€ std::overflow_error (arithmetic overflow) โ”œโ”€โ”€ std::underflow_error (arithmetic underflow) โ””โ”€โ”€ std::system_error (C++11) (OS/IO errors with error_code) โ””โ”€โ”€ std::ios_base::failure (I/O stream failure) std::bad_alloc (new fails โ€” out of memory) std::bad_cast (dynamic_cast to reference fails) std::bad_typeid (typeid on null pointer) std::bad_exception (unexpected exception in specification) std::bad_variant_access (wrong std::get on variant)
C++ โ€” Standard exceptions in practiceโ–ถ Run in Compiler
#include <iostream>
#include <stdexcept>
#include <vector>
#include <string>
#include <new>

void demonstrateStdExceptions() {
    // std::out_of_range (from vector::at)
    try {
        std::vector<int> v{1, 2, 3};
        v.at(10);  // throws out_of_range
    } catch (const std::out_of_range& e) {
        std::cout << "out_of_range: " << e.what() << "
";
    }

    // std::bad_alloc (out of memory)
    try {
        auto p = new int[1'000'000'000'000LL];  // try to allocate 1TB
        delete[] p;
    } catch (const std::bad_alloc& e) {
        std::cout << "bad_alloc: " << e.what() << "
";
    }

    // std::stoi โ€” std::invalid_argument
    try {
        int n = std::stoi("not_a_number");
    } catch (const std::invalid_argument& e) {
        std::cout << "stoi invalid: " << e.what() << "
";
    }

    // std::stoi โ€” std::out_of_range
    try {
        int n = std::stoi("99999999999999999");  // overflow
    } catch (const std::out_of_range& e) {
        std::cout << "stoi overflow: " << e.what() << "
";
    }

    // std::bad_cast
    try {
        class A { public: virtual ~A() {} };
        class B : public A {};
        A a;
        B& b = dynamic_cast<B&>(a);  // throws bad_cast (reference cast)
    } catch (const std::bad_cast& e) {
        std::cout << "bad_cast: " << e.what() << "
";
    }
}

int main() {
    demonstrateStdExceptions();
    return 0;
}
3Custom Exception Hierarchy
C++ โ€” Custom exception hierarchy for an appโ–ถ Run in Compiler
#include <iostream>
#include <stdexcept>
#include <string>

// โ”€โ”€โ”€ Application Exception Hierarchy โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
class AppException : public std::runtime_error {
    int errorCode_;
public:
    AppException(const std::string& msg, int code)
        : std::runtime_error(msg), errorCode_{code} {}
    int errorCode() const noexcept { return errorCode_; }
};

class NetworkException : public AppException {
    std::string endpoint_;
public:
    NetworkException(const std::string& msg, const std::string& endpoint, int code)
        : AppException(msg + " [" + endpoint + "]", code), endpoint_{endpoint} {}
    const std::string& endpoint() const noexcept { return endpoint_; }
};

class ConnectionRefused : public NetworkException {
public:
    explicit ConnectionRefused(const std::string& host, int port)
        : NetworkException("Connection refused",
                           host + ":" + std::to_string(port), 1001) {}
};

class Timeout : public NetworkException {
    int timeoutMs_;
public:
    Timeout(const std::string& endpoint, int ms)
        : NetworkException("Request timed out after " + std::to_string(ms) + "ms",
                           endpoint, 1002),
          timeoutMs_{ms} {}
    int timeoutMs() const noexcept { return timeoutMs_; }
};

class DatabaseException : public AppException {
    std::string query_;
public:
    DatabaseException(const std::string& msg, const std::string& query)
        : AppException(msg, 2001), query_{query} {}
    const std::string& query() const noexcept { return query_; }
};

// โ”€โ”€โ”€ Functions that throw โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
void connectToServer(const std::string& host, int port) {
    if (host == "badhost") throw ConnectionRefused(host, port);
    if (host == "slowhost") throw Timeout(host + ":" + std::to_string(port), 5000);
    std::cout << "Connected to " << host << ":" << port << "
";
}

void executeQuery(const std::string& sql) {
    if (sql.find("DROP TABLE") != std::string::npos)
        throw DatabaseException("Dangerous operation blocked", sql);
    std::cout << "Query OK: " << sql << "
";
}

void runApplication(const std::string& host) {
    try {
        connectToServer(host, 5432);
        executeQuery("SELECT * FROM users");
        executeQuery("DROP TABLE users");   // blocked!
    } catch (const ConnectionRefused& e) {
        std::cout << "[ERR " << e.errorCode() << "] Connection refused: " << e.what() << "
";
    } catch (const Timeout& e) {
        std::cout << "[ERR " << e.errorCode() << "] Timeout (" << e.timeoutMs() << "ms): " << e.what() << "
";
    } catch (const DatabaseException& e) {
        std::cout << "[ERR " << e.errorCode() << "] DB: " << e.what() << "
";
        std::cout << "  Query was: " << e.query() << "
";
    } catch (const AppException& e) {
        std::cout << "[ERR " << e.errorCode() << "] App: " << e.what() << "
";
    }
}

int main() {
    std::cout << "=== Connecting to badhost ===
";
    runApplication("badhost");
    std::cout << "
=== Connecting to slowhost ===
";
    runApplication("slowhost");
    std::cout << "
=== Connecting to localhost ===
";
    runApplication("localhost");
    return 0;
}
4Exception Safety & RAII

Exception Safety Guarantees (from weakest to strongest):

โ€ข No guarantee: Anything can happen on exception โ€” leaks, corruption. Never write this.

โ€ข Basic guarantee: No resource leaks (invariants preserved), but object may be in a valid but changed state.

โ€ข Strong guarantee (commit-or-rollback): Operation either completes fully or has zero effect on state. Use copy-and-swap idiom.

โ€ข No-throw guarantee (noexcept): Function never throws โ€” guaranteed. Required for move constructors, destructors, and swap().

C++ โ€” Exception safety, noexcept, copy-and-swapโ–ถ Run in Compiler
#include <iostream>
#include <memory>
#include <vector>
#include <stdexcept>

class SafeBuffer {
    std::unique_ptr<int[]> data_;
    std::size_t size_;
public:
    explicit SafeBuffer(std::size_t n)
        : data_{std::make_unique<int[]>(n)}, size_{n} {}

    // Strong guarantee: copy-and-swap assignment
    SafeBuffer& operator=(SafeBuffer other) noexcept {  // copy made, then swapped
        std::swap(data_, other.data_);
        std::swap(size_, other.size_);
        return *this;                        // if copy throws, this is unchanged!
    }

    // noexcept move (enables std::vector optimizations!)
    SafeBuffer(SafeBuffer&&) noexcept = default;
    SafeBuffer& operator=(SafeBuffer&&) noexcept = default;
    SafeBuffer(const SafeBuffer& other) : data_{std::make_unique<int[]>(other.size_)}, size_{other.size_} {
        std::copy(other.data_.get(), other.data_.get() + size_, data_.get());
    }

    int&       operator[](std::size_t i)       { return data_[i]; }
    const int& operator[](std::size_t i) const { return data_[i]; }
    std::size_t size() const noexcept { return size_; }

    // RAII โ€” automatically freed by unique_ptr destructor
};

// RAII lock guard example
class MutexLock {
    bool locked_{false};
public:
    MutexLock() noexcept { locked_ = true; std::cout << "Lock acquired
"; }
    ~MutexLock() noexcept { if (locked_) std::cout << "Lock released
"; }
    MutexLock(const MutexLock&) = delete;
    MutexLock& operator=(const MutexLock&) = delete;
};

void criticalSection() {
    MutexLock lock;          // RAII: released even if exception thrown!
    std::cout << "In critical section
";
    throw std::runtime_error("something went wrong");
    // lock destructor runs here even on exception โœ…
}

int main() {
    // SafeBuffer demo
    SafeBuffer buf(5);
    for (std::size_t i = 0; i < buf.size(); ++i) buf[i] = (int)(i * 10);

    // RAII exception safety
    try {
        criticalSection();
    } catch (const std::exception& e) {
        std::cout << "Caught: " << e.what() << "
";
    }
    std::cout << "Lock was properly released despite exception!
";

    // noexcept check
    std::cout << "SafeBuffer move noexcept: " << std::boolalpha
              << std::is_nothrow_move_constructible_v<SafeBuffer> << "
";
    return 0;
}
5Technical FAQs

Q1: Why catch by const reference?

Catching by value slices derived exceptions โ€” if a ConnectionRefused is thrown and caught as AppException by value, only the base class parts are copied. const reference preserves the full dynamic type and avoids copying the exception object.

Q2: Why shouldn't destructors throw?

If an exception is propagating and a destructor throws another exception during stack unwinding, std::terminate() is called immediately. C++11+ destructors are implicitly noexcept. Never let exceptions escape from destructors.

Q3: What is the performance cost of exceptions?

Zero-cost exception handling (ZEH): when no exception occurs, exception handling has zero runtime overhead. When an exception is thrown, there IS overhead (stack unwinding, RTTI lookup). Use exceptions for exceptional cases, not regular control flow.

Q4: What is std::exception_ptr?

std::exception_ptr stores a reference-counted copy of an exception. Captured with std::current_exception(), re-thrown with std::rethrow_exception(). Used to propagate exceptions across threads.

Q5: What does std::terminate call by default?

std::abort() โ€” terminates the program immediately without stack unwinding. You can replace it with std::set_terminate(handler) to log a final message before aborting, which is useful in production crash reporting.