Advanced Topics

Exception Handling

Master C++ exception handling — try/catch/throw, the exception hierarchy, custom exceptions, noexcept, RAII with exceptions, and error handling strategies.

Exception Handling

C++ uses exceptions to report and handle errors that cannot be dealt with locally. The throw statement signals an error; try/catch blocks handle it. Unlike return codes, exceptions cannot be silently ignored — if uncaught, they terminate the program. Combined with RAII, exceptions allow clean error handling without manual cleanup code.


Basic try / catch / throw

cpp
#include <iostream>
#include <stdexcept>
using namespace std;

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

int main() {
    try {
        cout << divide(10, 2) << "\n";   // 5
        cout << divide(10, 0) << "\n";   // Throws!
        cout << "This never runs\n";
    } catch (const invalid_argument& e) {
        cout << "Error: " << e.what() << "\n";   // Error: Division by zero
    }

    cout << "Program continues after catch\n";
    return 0;
}

The Standard Exception Hierarchy

std::exception
├── std::logic_error
│   ├── std::invalid_argument   — Bad argument value
│   ├── std::out_of_range       — Index/value out of valid range
│   ├── std::domain_error       — Input outside mathematical domain
│   └── std::length_error       — Attempt to exceed max size
├── std::runtime_error
│   ├── std::range_error        — Result cannot be represented
│   ├── std::overflow_error     — Arithmetic overflow
│   └── std::underflow_error    — Arithmetic underflow
├── std::bad_alloc              — Memory allocation failed
├── std::bad_cast               — dynamic_cast failed
└── std::bad_optional_access    — Empty optional accessed (C++17)
cpp
#include <iostream>
#include <stdexcept>
#include <vector>
using namespace std;

int main() {
    // out_of_range
    vector<int> v = {1, 2, 3};
    try {
        cout << v.at(10) << "\n";
    } catch (const out_of_range& e) {
        cout << "Out of range: " << e.what() << "\n";
    }

    // bad_alloc — allocating too much memory
    try {
        vector<int> huge(1000000000000LL);
    } catch (const bad_alloc& e) {
        cout << "Allocation failed: " << e.what() << "\n";
    }

    // invalid_argument
    try {
        int n = stoi("not_a_number");
    } catch (const invalid_argument& e) {
        cout << "Invalid: " << e.what() << "\n";
    }

    return 0;
}

Multiple catch Blocks

cpp
#include <iostream>
#include <stdexcept>
using namespace std;

void riskyOperation(int code) {
    switch (code) {
        case 1: throw invalid_argument("Bad argument");
        case 2: throw out_of_range("Out of range");
        case 3: throw runtime_error("Runtime failure");
        case 4: throw 42;              // Throwing a non-exception (avoid!)
        case 5: throw "C string";      // Avoid!
    }
}

int main() {
    for (int i = 1; i <= 5; i++) {
        try {
            riskyOperation(i);
        } catch (const invalid_argument& e) {
            cout << "Invalid arg: " << e.what() << "\n";
        } catch (const out_of_range& e) {
            cout << "Range error: " << e.what() << "\n";
        } catch (const runtime_error& e) {
            cout << "Runtime: " << e.what() << "\n";
        } catch (int n) {
            cout << "Integer thrown: " << n << "\n";
        } catch (...) {           // Catch-all — catches anything
            cout << "Unknown exception\n";
        }
    }

    return 0;
}

Catch blocks are checked in order — put more specific exceptions before more general ones. Since std::exception is the base of all standard exceptions, put it last:

cpp
try { ... }
catch (const out_of_range& e)  { ... }   // More specific
catch (const logic_error& e)   { ... }   // Includes out_of_range
catch (const std::exception& e){ ... }   // Catches all standard exceptions
catch (...)                     { ... }   // Catches anything else

Custom Exceptions

cpp
#include <iostream>
#include <stdexcept>
#include <string>
using namespace std;

// Custom exception hierarchy
class AppError : public runtime_error {
    int errorCode;
public:
    AppError(const string& msg, int code)
        : runtime_error(msg), errorCode(code) {}

    int code() const { return errorCode; }
};

class DatabaseError : public AppError {
    string query;
public:
    DatabaseError(const string& msg, const string& q)
        : AppError(msg, 500), query(q) {}

    const string& failedQuery() const { return query; }
};

class AuthError : public AppError {
public:
    AuthError(const string& msg) : AppError(msg, 401) {}
};

void connect(const string& user, const string& pass) {
    if (user.empty()) throw AppError("Empty username", 400);
    if (pass == "wrong") throw AuthError("Invalid credentials");
    if (user == "dbuser") throw DatabaseError("Connection refused",
                                               "CONNECT 'main_db'");
}

int main() {
    vector<pair<string,string>> attempts = {
        {"", "pass"},
        {"admin", "wrong"},
        {"dbuser", "correct"},
        {"alice", "correct"},
    };

    for (const auto& [user, pass] : attempts) {
        try {
            connect(user, pass);
            cout << user << ": Connected successfully\n";
        } catch (const DatabaseError& e) {
            cout << "DB Error [" << e.code() << "]: " << e.what()
                 << " | Query: " << e.failedQuery() << "\n";
        } catch (const AuthError& e) {
            cout << "Auth Error [" << e.code() << "]: " << e.what() << "\n";
        } catch (const AppError& e) {
            cout << "App Error [" << e.code() << "]: " << e.what() << "\n";
        }
    }
    // App Error [400]: Empty username
    // Auth Error [401]: Invalid credentials
    // DB Error [500]: Connection refused | Query: CONNECT 'main_db'
    // alice: Connected successfully

    return 0;
}

RAII and Exception Safety

RAII ensures resources are freed even when exceptions are thrown — no try/finally needed:

cpp
#include <iostream>
#include <fstream>
#include <stdexcept>
using namespace std;

void processFile(const string& filename) {
    ifstream file(filename);   // RAII — destructor closes file
    if (!file.is_open()) {
        throw runtime_error("Cannot open: " + filename);
    }

    string line;
    while (getline(file, line)) {
        if (line.find("ERROR") != string::npos) {
            throw runtime_error("Error found in log: " + line);
            // file is closed automatically here — even on exception!
        }
        cout << line << "\n";
    }
}   // file destructor runs here even if exception was thrown

int main() {
    try {
        processFile("app.log");
    } catch (const runtime_error& e) {
        cerr << "Processing failed: " << e.what() << "\n";
    }
    return 0;
}

noexcept

noexcept declares that a function will never throw — allows the compiler to optimise:

cpp
#include <iostream>
using namespace std;

// Unconditional noexcept — always does not throw
int safeAdd(int a, int b) noexcept {
    return a + b;
}

// Conditional noexcept — only noexcept if T's operations are noexcept
template <typename T>
void moveData(T& dest, T& src) noexcept(noexcept(T(move(src)))) {
    dest = move(src);
}

// Check at compile time
static_assert(noexcept(safeAdd(1, 2)), "safeAdd must not throw");

class Widget {
public:
    Widget(Widget&& other) noexcept = default;  // Ensures vector uses move
    Widget& operator=(Widget&&) noexcept = default;
};

int main() {
    cout << noexcept(safeAdd(1, 2)) << "\n";   // 1 (true)
    return 0;
}

Warning: If a noexcept function does throw, std::terminate() is called immediately — no unwinding, no cleanup. Only mark functions noexcept if you are certain they cannot throw.


Exception Safety Guarantees

GuaranteeMeaning
No-throwFunction never throws (noexcept)
StrongEither succeeds completely or has no effect (commit or rollback)
BasicValid state preserved; some data may have changed
No guaranteeObject may be in invalid state after exception
cpp
// Strong guarantee — copy-and-swap pattern
vector<int>& operator=(const vector<int>& other) {
    vector<int> temp(other);   // Copy — if this throws, original is unchanged
    swap(data_, temp.data_);   // noexcept swap — no exception possible
    return *this;              // Strong guarantee achieved
}

Rethrowing Exceptions

cpp
#include <iostream>
#include <stdexcept>
using namespace std;

void logAndRethrow() {
    try {
        throw runtime_error("Original error");
    } catch (const exception& e) {
        cerr << "Logging: " << e.what() << "\n";
        throw;   // Rethrow the SAME exception (preserves type and message)
    }
}

int main() {
    try {
        logAndRethrow();
    } catch (const runtime_error& e) {
        cout << "Caught: " << e.what() << "\n";  // Original error
    }
    return 0;
}

Practical Example: Configuration Parser

cpp
#include <iostream>
#include <fstream>
#include <sstream>
#include <map>
#include <stdexcept>
#include <string>
using namespace std;

class ParseError : public runtime_error {
    int lineNum;
public:
    ParseError(const string& msg, int line)
        : runtime_error(msg + " (line " + to_string(line) + ")"),
          lineNum(line) {}
    int line() const { return lineNum; }
};

class Config {
    map<string, string> values;

public:
    void parse(const string& content) {
        istringstream ss(content);
        string line;
        int lineNum = 0;

        while (getline(ss, line)) {
            lineNum++;
            // Skip empty lines and comments
            if (line.empty() || line[0] == '#') continue;

            auto eq = line.find('=');
            if (eq == string::npos) {
                throw ParseError("Missing '=' in: " + line, lineNum);
            }

            string key = line.substr(0, eq);
            string val = line.substr(eq + 1);

            // Trim whitespace
            auto trim = [](string& s) {
                s.erase(0, s.find_first_not_of(" \t"));
                s.erase(s.find_last_not_of(" \t") + 1);
            };
            trim(key); trim(val);

            if (key.empty()) {
                throw ParseError("Empty key", lineNum);
            }
            values[key] = val;
        }
    }

    string get(const string& key, const string& def = "") const {
        auto it = values.find(key);
        return (it != values.end()) ? it->second : def;
    }

    int getInt(const string& key) const {
        auto it = values.find(key);
        if (it == values.end()) throw out_of_range("Key not found: " + key);
        try {
            return stoi(it->second);
        } catch (const invalid_argument&) {
            throw invalid_argument("'" + key + "' is not an integer: " + it->second);
        }
    }
};

int main() {
    Config cfg;

    string goodConfig = R"(
# Database settings
host = localhost
port = 5432
name = mydb
max_connections = 100
)";

    string badConfig = R"(
host = localhost
no_equals_here
port = 5432
)";

    try {
        cfg.parse(goodConfig);
        cout << "host: " << cfg.get("host")         << "\n";  // localhost
        cout << "port: " << cfg.getInt("port")       << "\n";  // 5432
        cout << "max:  " << cfg.getInt("max_connections") << "\n";  // 100
        cout << "user: " << cfg.get("user", "admin") << "\n";  // admin (default)
    } catch (const exception& e) {
        cerr << "Config error: " << e.what() << "\n";
    }

    cout << "\n--- Testing bad config ---\n";
    try {
        Config bad;
        bad.parse(badConfig);
    } catch (const ParseError& e) {
        cerr << "Parse error at line " << e.line() << ": " << e.what() << "\n";
        // Parse error at line 3: Missing '=' in: no_equals_here (line 3)
    }

    return 0;
}

Summary

  • throw signals an error; try/catch handles it — exceptions propagate up the call stack until caught
  • Catch blocks are checked in order — always catch more specific exceptions before general ones
  • Catch by const reference (const SomeException& e) — never by value (avoids slicing)
  • The standard exception hierarchy is rooted at std::exception — always derive custom exceptions from it
  • RAII ensures resources are cleaned up even when exceptions are thrown — no finally needed in C++
  • noexcept tells the compiler a function cannot throw — required for move operations used by containers
  • The strong exception guarantee (commit or rollback) is achieved with copy-and-swap
  • Avoid throwing non-class types (integers, C strings) — always throw objects derived from std::exception

Next, we'll explore file handling — reading and writing files with C++ streams.