Unit 6.0: Exception Handling ⚠️

1. Introduction to Exception Handling

  • An exception is an unexpected event or error that disrupts the normal flow of a program (e.g., dividing by zero, file not found, invalid input, out-of-memory).

Without Exception Handling (Bad Practice)

#include <iostream>
using namespace std;
 
// Old style: use return codes — messy and error-prone
int divide(int a, int b, int& result) {
    if (b == 0) return -1;    // error code
    result = a / b;
    return 0;                 // success
}
 
int main() {
    int result;
    int status = divide(10, 0, result);
    if (status == -1) {
        cout << "Error: division by zero" << endl;
    }
    // Caller must always check return value — easy to forget!
    return 0;
}

With Exception Handling (Clean)

#include <iostream>
#include <stdexcept>
using namespace std;
 
int divide(int a, int b) {
    if (b == 0)
        throw invalid_argument("Division by zero!");
    return a / b;
}
 
int main() {
    try {
        cout << divide(10, 2) << endl;   // 5 (ok)
        cout << divide(10, 0) << endl;   // throws!
    } catch (const invalid_argument& e) {
        cout << "Error: " << e.what() << endl;
    }
    cout << "Program continues normally." << endl;
    return 0;
}

Exception Handling Flow

Normal Flow:
   try block → execute statements → (no exception) → continue after try-catch

Exception Flow:
   try block → statement throws → remaining try skipped
             → matching catch block executes
             → program continues after the catch block

2. try, throw, and catch

The Three Keywords

KeywordPurpose
tryWraps code that might throw an exception
throwCreates and sends an exception
catchHandles a thrown exception

Basic Syntax

try {
    // Risky code
    throw SomeExceptionType("message");
}
catch (const SomeExceptionType& e) {
    // Handle it
    cout << e.what() << endl;
}
catch (...) {
    // Catch-all: handles ANY exception
    cout << "Unknown exception caught!" << endl;
}

Throwing Different Types

#include <iostream>
#include <stdexcept>
#include <string>
using namespace std;
 
void checkAge(int age) {
    if (age < 0 || age > 150)
        throw out_of_range("Age must be between 0 and 150");
}
 
void checkDivision(double a, double b) {
    if (b == 0.0)
        throw invalid_argument("Divisor cannot be zero");
}
 
void openFile(const string& filename) {
    if (filename.empty())
        throw runtime_error("Filename cannot be empty");
    // simulate file not found
    if (filename == "missing.txt")
        throw runtime_error("File not found: " + filename);
}
 
int main() {
    // Test age check
    try {
        checkAge(25);
        cout << "Age 25: OK" << endl;
        checkAge(-5);   // throws
    } catch (const out_of_range& e) {
        cout << "Range error: " << e.what() << endl;
    }
 
    // Test division
    try {
        checkDivision(10.0, 2.0);
        cout << "Division: OK" << endl;
        checkDivision(5.0, 0.0);   // throws
    } catch (const invalid_argument& e) {
        cout << "Argument error: " << e.what() << endl;
    }
 
    // Test file
    try {
        openFile("missing.txt");
    } catch (const runtime_error& e) {
        cout << "Runtime error: " << e.what() << endl;
    }
 
    cout << "Program completed." << endl;
    return 0;
}
#include <iostream>
using namespace std;
 
int main() {
    // Can throw any type, but prefer exception classes
    try {
        throw 42;           // throw int
    } catch (int code) {
        cout << "Error code: " << code << endl;
    }
 
    try {
        throw "Something went wrong";  // throw string literal
    } catch (const char* msg) {
        cout << "Error: " << msg << endl;
    }
 
    // Better: use standard exception types (shown in section 3 & 6)
    return 0;
}

3. Creating Custom Exception Classes

You can create exception classes tailored to your application by inheriting from std::exception or any of its children.

Simple Custom Exception

#include <iostream>
#include <exception>
#include <string>
using namespace std;
 
// Custom exception: inherit from std::exception
class ValidationError : public exception {
    string message;
public:
    explicit ValidationError(const string& msg) : message(msg) {}
 
    // Override what() — returns the error message
    const char* what() const noexcept override {
        return message.c_str();
    }
};
 
void validateUsername(const string& name) {
    if (name.length() < 3)
        throw ValidationError("Username too short (min 3 chars)");
    if (name.length() > 20)
        throw ValidationError("Username too long (max 20 chars)");
    for (char c : name) {
        if (!isalnum(c) && c != '_')
            throw ValidationError("Username contains invalid character: " + string(1, c));
    }
}
 
int main() {
    string users[] = {"ok", "valid_user", "ab", "has space", "good123"};
 
    for (const string& user : users) {
        try {
            validateUsername(user);
            cout << "\"" << user << "\" is VALID" << endl;
        } catch (const ValidationError& e) {
            cout << "\"" << user << "\" INVALID: " << e.what() << endl;
        }
    }
    return 0;
}

Exception Hierarchy

#include <iostream>
#include <exception>
#include <string>
using namespace std;
 
// Base application exception
class AppException : public exception {
protected:
    string message;
    int code;
public:
    AppException(const string& msg, int code = 0)
        : message(msg), code(code) {}
 
    const char* what() const noexcept override { return message.c_str(); }
    int getCode() const { return code; }
};
 
// Derived: database errors
class DatabaseException : public AppException {
    string query;
public:
    DatabaseException(const string& msg, const string& q, int code = 1000)
        : AppException(msg, code), query(q) {}
 
    string getQuery() const { return query; }
 
    const char* what() const noexcept override {
        // Return enhanced message
        static string full;
        full = "[DB Error " + to_string(code) + "] " + message + " | Query: " + query;
        return full.c_str();
    }
};
 
// Derived: network errors
class NetworkException : public AppException {
    int statusCode;
public:
    NetworkException(const string& msg, int status, int code = 2000)
        : AppException(msg, code), statusCode(status) {}
 
    int getStatusCode() const { return statusCode; }
};
 
// Derived: auth errors
class AuthException : public AppException {
public:
    AuthException(const string& msg)
        : AppException(msg, 3000) {}
};
 
void simulateDB() {
    throw DatabaseException("Table not found", "SELECT * FROM nonexistent", 1042);
}
 
void simulateAuth() {
    throw AuthException("Invalid credentials");
}
 
int main() {
    // Catch specific types
    try {
        simulateDB();
    } catch (const DatabaseException& e) {
        cout << e.what() << endl;
        cout << "Error code: " << e.getCode() << endl;
    }
 
    try {
        simulateAuth();
    } catch (const AuthException& e) {
        cout << "Auth failed: " << e.what() << endl;
    }
 
    // Catch base class — handles any AppException
    try {
        simulateDB();
    } catch (const AppException& e) {
        cout << "App error [" << e.getCode() << "]: " << e.what() << endl;
    }
 
    return 0;
}

4. Exception Handling Techniques

4.1 Terminate the Program

Used when the error is fatal and recovery is impossible. The program logs the error and exits cleanly.

#include <iostream>
#include <fstream>
#include <stdexcept>
#include <cstdlib>   // for exit()
using namespace std;
 
void logError(const string& msg) {
    cerr << "[FATAL] " << msg << endl;
    // Could write to log file here
    ofstream log("error_log.txt", ios::app);
    if (log.is_open()) {
        log << "[FATAL] " << msg << endl;
    }
}
 
int* allocateLargeBuffer(size_t size) {
    int* buf = new(nothrow) int[size];
    if (!buf) {
        throw bad_alloc();
    }
    return buf;
}
 
int main() {
    try {
        // Critical initialization code
        // allocateLargeBuffer(1000000000000ULL);  // Would fail
 
        cout << "System initialized successfully." << endl;
        // ... rest of program ...
    }
    catch (const bad_alloc& e) {
        logError("Out of memory: " + string(e.what()));
        exit(EXIT_FAILURE);   // terminate with error code
    }
    catch (const exception& e) {
        logError("Fatal error: " + string(e.what()));
        exit(EXIT_FAILURE);
    }
    catch (...) {
        logError("Unknown fatal error!");
        exit(EXIT_FAILURE);
    }
 
    return 0;
}

4.2 Fix the Error and Continue

The exception is caught, corrected, and execution continues normally.

#include <iostream>
#include <stdexcept>
#include <string>
using namespace std;
 
class Config {
    int maxRetries;
    double timeout;
    string logLevel;
 
public:
    Config() : maxRetries(3), timeout(30.0), logLevel("INFO") {}
 
    void setMaxRetries(int n) {
        if (n <= 0)
            throw invalid_argument("maxRetries must be positive");
        maxRetries = n;
    }
 
    void setTimeout(double t) {
        if (t <= 0)
            throw invalid_argument("timeout must be positive");
        timeout = t;
    }
 
    void setLogLevel(const string& level) {
        if (level != "DEBUG" && level != "INFO" &&
            level != "WARN"  && level != "ERROR")
            throw invalid_argument("Invalid log level: " + level);
        logLevel = level;
    }
 
    void display() const {
        cout << "Config: retries=" << maxRetries
             << " timeout=" << timeout
             << " level=" << logLevel << endl;
    }
};
 
int main() {
    Config cfg;
 
    // Attempt to set values, fix invalid ones automatically
    struct { string key; string value; } settings[] = {
        {"retries", "5"},
        {"retries", "-2"},      // invalid → fix to default
        {"timeout", "60"},
        {"timeout", "-10"},     // invalid → fix to default
        {"loglevel", "VERBOSE"} // invalid → fix to default
    };
 
    for (auto& s : settings) {
        try {
            if (s.key == "retries")  cfg.setMaxRetries(stoi(s.value));
            if (s.key == "timeout")  cfg.setTimeout(stod(s.value));
            if (s.key == "loglevel") cfg.setLogLevel(s.value);
        }
        catch (const invalid_argument& e) {
            cout << "Invalid value for " << s.key << " (\"" << s.value << "\")"
                 << ": " << e.what() << " → keeping default." << endl;
            // DON'T rethrow — just continue with default value
        }
    }
 
    cfg.display();
    return 0;
}

4.3 Log the Error and Continue

The exception is caught, recorded in a log, and execution moves on to the next item.

#include <iostream>
#include <fstream>
#include <vector>
#include <stdexcept>
#include <chrono>
#include <ctime>
using namespace std;
 
class Logger {
    ofstream logFile;
    bool toConsole;
 
public:
    Logger(const string& filename = "", bool console = true)
        : toConsole(console) {
        if (!filename.empty()) {
            logFile.open(filename, ios::app);
        }
    }
 
    void log(const string& level, const string& msg) {
        auto now = chrono::system_clock::now();
        time_t t = chrono::system_clock::to_time_t(now);
        string timeStr(ctime(&t));
        timeStr.pop_back();  // remove newline
 
        string entry = "[" + timeStr + "][" + level + "] " + msg;
 
        if (toConsole) cout << entry << endl;
        if (logFile.is_open()) logFile << entry << "\n";
    }
 
    void error(const string& msg) { log("ERROR", msg); }
    void warn(const string& msg)  { log("WARN",  msg); }
    void info(const string& msg)  { log("INFO",  msg); }
};
 
// Simulate processing orders
struct Order {
    int id;
    string product;
    int quantity;
};
 
double processOrder(const Order& order) {
    if (order.quantity <= 0)
        throw invalid_argument("Quantity must be positive for order #" +
                               to_string(order.id));
    if (order.product.empty())
        throw runtime_error("Product name is empty for order #" +
                            to_string(order.id));
    return order.quantity * 9.99;   // price per unit
}
 
int main() {
    Logger logger("", true);  // console only for this demo
 
    vector<Order> orders = {
        {1001, "Widget",  5},
        {1002, "",        3},    // invalid: empty product
        {1003, "Gadget", -1},    // invalid: negative quantity
        {1004, "Donut",   10},
        {1005, "Gizmo",   0}     // invalid: zero quantity
    };
 
    double totalRevenue = 0.0;
    int processed = 0, errors = 0;
 
    for (const Order& order : orders) {
        try {
            double revenue = processOrder(order);
            totalRevenue += revenue;
            processed++;
            logger.info("Order #" + to_string(order.id) +
                       " processed. Revenue: $" + to_string(revenue));
        }
        catch (const invalid_argument& e) {
            errors++;
            logger.error(string(e.what()));
            // Continue with next order
        }
        catch (const runtime_error& e) {
            errors++;
            logger.error(string(e.what()));
            // Continue with next order
        }
    }
 
    cout << "\n=== Summary ===" << endl;
    cout << "Processed: " << processed << " | Errors: " << errors << endl;
    cout << "Total Revenue: $" << totalRevenue << endl;
    return 0;
}

5. Stack Unwinding

When an exception is thrown, C++ unwinds the call stack — it goes back through each function call that led to the throw, calling destructors for local objects along the way.

Visualizing Stack Unwinding

#include <iostream>
#include <stdexcept>
using namespace std;
 
class Resource {
    string name;
public:
    Resource(string n) : name(n) {
        cout << "  [Acquired] " << name << endl;
    }
    ~Resource() {
        cout << "  [Released] " << name << " (stack unwinding)" << endl;
    }
    void use() { cout << "  [Using]    " << name << endl; }
};
 
void functionC() {
    Resource r3("DB Connection");
    r3.use();
    cout << "  → functionC: about to throw!" << endl;
    throw runtime_error("Something went wrong in C!");
    // r3 destructor will be called during unwinding
}
 
void functionB() {
    Resource r2("File Handle");
    r2.use();
    cout << "→ functionB: calling C" << endl;
    functionC();   // exception propagates here
    // r2 destructor will be called during unwinding
    cout << "→ functionB: after C (never reached)" << endl;
}
 
void functionA() {
    Resource r1("Memory Buffer");
    r1.use();
    cout << "→ functionA: calling B" << endl;
    functionB();   // exception propagates here
    // r1 destructor will be called during unwinding
    cout << "→ functionA: after B (never reached)" << endl;
}
 
int main() {
    cout << "=== Stack Unwinding Demo ===" << endl;
    try {
        functionA();
    }
    catch (const runtime_error& e) {
        cout << "\nCaught in main: " << e.what() << endl;
    }
    cout << "\nProgram continues after exception." << endl;
    return 0;
}
/*
Output:
=== Stack Unwinding Demo ===
  [Acquired] Memory Buffer
  [Using]    Memory Buffer
→ functionA: calling B
  [Acquired] File Handle
  [Using]    File Handle
→ functionB: calling C
  [Acquired] DB Connection
  [Using]    DB Connection
  → functionC: about to throw!
  [Released] DB Connection (stack unwinding)
  [Released] File Handle (stack unwinding)
  [Released] Memory Buffer (stack unwinding)
 
Caught in main: Something went wrong in C!
 
Program continues after exception.
*/

Stack Unwinding Guarantees (RAII)

RAII (Resource Acquisition Is Initialization) is the principle that resources are automatically released when a variable goes out of scope — even during exception unwinding.

#include <iostream>
#include <fstream>
#include <stdexcept>
using namespace std;
 
// RAII wrapper for a mutex-like lock
class ScopedLock {
    string resource;
public:
    explicit ScopedLock(const string& r) : resource(r) {
        cout << "Locked: " << resource << endl;
    }
    ~ScopedLock() {
        // ALWAYS runs — even when exception thrown!
        cout << "Unlocked: " << resource << endl;
    }
};
 
void riskyOperation(bool fail) {
    ScopedLock lock("SharedFile");   // lock is acquired
 
    cout << "Doing work..." << endl;
    if (fail) {
        throw runtime_error("Operation failed!");
    }
    cout << "Work done." << endl;
    // lock released here normally, OR during unwinding if exception thrown
}
 
int main() {
    cout << "--- Success case ---" << endl;
    try {
        riskyOperation(false);
    } catch (const exception& e) {
        cout << "Caught: " << e.what() << endl;
    }
 
    cout << "\n--- Failure case ---" << endl;
    try {
        riskyOperation(true);
    } catch (const exception& e) {
        cout << "Caught: " << e.what() << endl;
    }
    // Lock is ALWAYS released — no resource leak!
    return 0;
}

6. Standard Exception Classes

C++ provides a rich hierarchy of standard exception classes.

std::exception
│
├── std::logic_error          (detectable before runtime)
│   ├── std::invalid_argument  (invalid argument passed)
│   ├── std::domain_error      (math domain error)
│   ├── std::length_error      (exceeds max allowed size)
│   └── std::out_of_range      (access out of valid range)
│
└── std::runtime_error        (only detectable at runtime)
    ├── std::overflow_error    (arithmetic overflow)
    ├── std::underflow_error   (arithmetic underflow)
    ├── std::range_error       (result out of range)
    └── std::bad_alloc         (memory allocation failed)

Using Standard Exceptions

#include <iostream>
#include <stdexcept>
#include <vector>
#include <string>
using namespace std;
 
class SafeVector {
    vector<int> data;
public:
    void add(int val) {
        data.push_back(val);
    }
 
    int get(int index) const {
        if (index < 0 || index >= (int)data.size())
            throw out_of_range("Index " + to_string(index) +
                               " out of range [0, " + to_string(data.size()-1) + "]");
        return data[index];
    }
 
    int size() const { return data.size(); }
};
 
double safeSqrt(double x) {
    if (x < 0)
        throw domain_error("Square root of negative number: " + to_string(x));
    return sqrt(x);
}
 
void processInput(const string& input) {
    if (input.empty())
        throw invalid_argument("Input string cannot be empty");
    // process...
}
 
int main() {
    // out_of_range
    SafeVector sv;
    sv.add(10); sv.add(20); sv.add(30);
    try {
        cout << sv.get(1) << endl;   // 20 — ok
        cout << sv.get(5) << endl;   // throws
    } catch (const out_of_range& e) {
        cout << "out_of_range: " << e.what() << endl;
    }
 
    // domain_error
    try {
        cout << safeSqrt(16) << endl;    // 4 — ok
        cout << safeSqrt(-9) << endl;    // throws
    } catch (const domain_error& e) {
        cout << "domain_error: " << e.what() << endl;
    }
 
    // invalid_argument
    try {
        processInput("hello");  // ok
        processInput("");       // throws
    } catch (const invalid_argument& e) {
        cout << "invalid_argument: " << e.what() << endl;
    }
 
    // bad_alloc
    try {
        // Attempt to allocate an impossibly large amount
        // vector<int> v(1000000000000LL);  // bad_alloc
    } catch (const bad_alloc& e) {
        cout << "bad_alloc: out of memory" << endl;
    }
 
    return 0;
}

7. Multiple catch Blocks

You can have multiple catch blocks to handle different exception types differently.

#include <iostream>
#include <stdexcept>
#include <string>
using namespace std;
 
void riskyFunction(int mode) {
    switch (mode) {
        case 1: throw invalid_argument("Invalid argument provided");
        case 2: throw out_of_range("Value out of allowed range");
        case 3: throw runtime_error("Runtime failure occurred");
        case 4: throw logic_error("Logic error in calculation");
        case 5: throw 42;                   // int exception (unusual)
        case 6: throw string("string error"); // string exception (unusual)
    }
}
 
int main() {
    for (int mode = 0; mode <= 7; mode++) {
        try {
            cout << "Mode " << mode << ": ";
            riskyFunction(mode);
            cout << "No exception." << endl;
        }
        catch (const invalid_argument& e) {
            cout << "[invalid_argument] " << e.what() << endl;
        }
        catch (const out_of_range& e) {
            cout << "[out_of_range] " << e.what() << endl;
        }
        catch (const runtime_error& e) {
            cout << "[runtime_error] " << e.what() << endl;
        }
        catch (const logic_error& e) {
            // catches any logic_error (and its children)
            cout << "[logic_error] " << e.what() << endl;
        }
        catch (const exception& e) {
            // catches any std::exception
            cout << "[exception] " << e.what() << endl;
        }
        catch (int code) {
            cout << "[int thrown] " << code << endl;
        }
        catch (const string& s) {
            cout << "[string thrown] " << s << endl;
        }
        catch (...) {
            // catch-all: handles ANYTHING
            cout << "[unknown exception]" << endl;
        }
    }
    return 0;
}

Order matters! Always catch more specific types before less specific types.

Correct order: invalid_argumentlogic_errorexception Wrong order: exception first would swallow everything!


8. Re-throwing Exceptions

Sometimes you catch an exception to do partial handling (e.g., logging), then re-throw it for a higher-level handler.

#include <iostream>
#include <stdexcept>
#include <string>
using namespace std;
 
class TransactionLogger {
public:
    static void logFailure(const string& context, const exception& e) {
        cerr << "[LOG] Transaction failed in " << context
             << ": " << e.what() << endl;
    }
};
 
void processPayment(double amount) {
    if (amount <= 0)
        throw invalid_argument("Payment amount must be positive");
    if (amount > 10000)
        throw out_of_range("Payment exceeds daily limit of $10,000");
    cout << "Payment of $" << amount << " processed." << endl;
}
 
void handlePayment(double amount) {
    try {
        processPayment(amount);
    }
    catch (const exception& e) {
        // Partial handling: log it
        TransactionLogger::logFailure("handlePayment", e);
        // Re-throw to let caller decide what to do
        throw;   // re-throw the SAME exception (not a copy)
    }
}
 
int main() {
    // Test case 1: valid
    try {
        handlePayment(500.0);
    }
    catch (const exception& e) {
        cout << "Payment failed: " << e.what() << endl;
    }
 
    // Test case 2: negative
    try {
        handlePayment(-100.0);
    }
    catch (const invalid_argument& e) {
        cout << "Invalid payment: " << e.what() << endl;
    }
 
    // Test case 3: too large
    try {
        handlePayment(15000.0);
    }
    catch (const out_of_range& e) {
        cout << "Limit exceeded: " << e.what() << endl;
    }
 
    return 0;
}

9. noexcept Specifier

noexcept marks a function as guaranteed not to throw. It improves performance and enables compiler optimizations.

#include <iostream>
#include <stdexcept>
using namespace std;
 
// Guaranteed not to throw — compiler can optimize
double add(double a, double b) noexcept {
    return a + b;
}
 
// May throw
double safeDivide(double a, double b) {
    if (b == 0.0)
        throw invalid_argument("Division by zero");
    return a / b;
}
 
class MyClass {
    int* data;
    int size;
public:
    MyClass(int s) : size(s), data(new int[s]) {}
 
    // Move constructor: should never throw
    MyClass(MyClass&& other) noexcept
        : data(other.data), size(other.size) {
        other.data = nullptr;
        other.size = 0;
    }
 
    // Move assignment: should never throw
    MyClass& operator=(MyClass&& other) noexcept {
        if (this != &other) {
            delete[] data;
            data = other.data;
            size = other.size;
            other.data = nullptr;
            other.size = 0;
        }
        return *this;
    }
 
    ~MyClass() noexcept { delete[] data; }  // destructors should always be noexcept
};
 
// noexcept(expr) — conditionally noexcept
template <typename T>
void swapValues(T& a, T& b) noexcept(noexcept(T(move(a)))) {
    T temp = move(a);
    a = move(b);
    b = move(temp);
}
 
int main() {
    cout << add(3.0, 4.0) << endl;   // 7
 
    try {
        cout << safeDivide(10.0, 2.0) << endl;  // 5
        cout << safeDivide(10.0, 0.0) << endl;  // throws
    } catch (const invalid_argument& e) {
        cout << e.what() << endl;
    }
 
    // Check if a function is noexcept
    cout << noexcept(add(1.0, 2.0)) << endl;         // 1 (true)
    cout << noexcept(safeDivide(1.0, 2.0)) << endl;  // 0 (false)
    return 0;
}

10. Practical Examples

Example 1: Safe File Reader

#include <iostream>
#include <fstream>
#include <stdexcept>
#include <string>
#include <vector>
using namespace std;
 
class FileReader {
    string filename;
 
public:
    explicit FileReader(const string& fname) : filename(fname) {}
 
    vector<string> readLines() const {
        ifstream file(filename);
        if (!file.is_open())
            throw runtime_error("Cannot open file: " + filename);
 
        vector<string> lines;
        string line;
        while (getline(file, line)) {
            lines.push_back(line);
        }
 
        if (lines.empty())
            throw runtime_error("File is empty: " + filename);
 
        return lines;
    }
};
 
int main() {
    vector<string> filenames = {"data.txt", "missing.txt", "config.txt"};
 
    for (const string& fname : filenames) {
        try {
            FileReader reader(fname);
            auto lines = reader.readLines();
            cout << fname << ": " << lines.size() << " lines read." << endl;
        }
        catch (const runtime_error& e) {
            cout << "[Error] " << e.what() << " — skipping." << endl;
        }
    }
    return 0;
}

Example 2: Bank Transaction System

#include <iostream>
#include <stdexcept>
#include <string>
using namespace std;
 
// Custom exceptions for banking
class BankException : public exception {
protected:
    string msg;
public:
    explicit BankException(const string& m) : msg(m) {}
    const char* what() const noexcept override { return msg.c_str(); }
};
 
class InsufficientFundsException : public BankException {
    double available, requested;
public:
    InsufficientFundsException(double avail, double req)
        : BankException("Insufficient funds"),
          available(avail), requested(req) {
        msg = "Insufficient funds: requested $" + to_string(req)
            + " but only $" + to_string(avail) + " available";
    }
    double getShortfall() const { return requested - available; }
};
 
class AccountLockedException : public BankException {
public:
    explicit AccountLockedException()
        : BankException("Account is locked. Contact customer support.") {}
};
 
class BankAccount {
    double balance;
    bool locked;
    string owner;
    int failedAttempts;
 
public:
    BankAccount(string name, double initial)
        : owner(name), balance(initial), locked(false), failedAttempts(0) {}
 
    void deposit(double amount) {
        if (locked) throw AccountLockedException();
        if (amount <= 0)
            throw invalid_argument("Deposit amount must be positive");
        balance += amount;
        cout << "Deposited $" << amount << ". Balance: $" << balance << endl;
    }
 
    void withdraw(double amount) {
        if (locked) throw AccountLockedException();
        if (amount <= 0)
            throw invalid_argument("Withdrawal amount must be positive");
        if (amount > balance)
            throw InsufficientFundsException(balance, amount);
        balance -= amount;
        cout << "Withdrew $" << amount << ". Balance: $" << balance << endl;
    }
 
    double getBalance() const { return balance; }
};
 
int main() {
    BankAccount acc("Alice", 1000.0);
 
    // Normal operations
    try {
        acc.deposit(500.0);
        acc.withdraw(200.0);
        acc.withdraw(2000.0);    // throws InsufficientFunds
    }
    catch (const InsufficientFundsException& e) {
        cout << "Error: " << e.what() << endl;
        cout << "Shortfall: $" << e.getShortfall() << endl;
    }
    catch (const invalid_argument& e) {
        cout << "Invalid: " << e.what() << endl;
    }
    catch (const BankException& e) {
        cout << "Bank error: " << e.what() << endl;
    }
 
    cout << "Final balance: $" << acc.getBalance() << endl;
    return 0;
}

Quick Reference Summary

Exception Handling Template

try {
    // Code that might throw
    throw ExceptionType("message");
}
catch (const SpecificException& e) {
    // Handle specific type
    cout << e.what();
}
catch (const BaseException& e) {
    // Handle base type (more general)
}
catch (...) {
    // Handle anything else
}

Creating a Custom Exception

class MyException : public exception {
    string message;
public:
    explicit MyException(const string& msg) : message(msg) {}
    const char* what() const noexcept override { return message.c_str(); }
};

Key Rules Cheat Sheet

RuleDetail
Catch by const referencecatch(const MyEx& e) — avoids slicing and copying
Order of catch blocksMost specific first, most general last
Always inherit from std::exceptionCustom exceptions must have what()
noexcept on destructorsDestructors should never throw
noexcept on move opsMove constructor/assignment should be noexcept
Re-throw with throw;Re-throws same exception (not throw e;)
RAIIUse destructors to release resources — safe under unwinding
Catch-all catch(...)Always last — use for logging/cleanup