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
#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)#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
#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:
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 elseCustom Exceptions
#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:
#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:
#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
| Guarantee | Meaning |
|---|---|
| No-throw | Function never throws (noexcept) |
| Strong | Either succeeds completely or has no effect (commit or rollback) |
| Basic | Valid state preserved; some data may have changed |
| No guarantee | Object may be in invalid state after exception |
// 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
#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
#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
throwsignals an error;try/catchhandles 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
finallyneeded in C++ noexcepttells 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.