C++ Exception Handling: try, catch, throw, Custom Exceptions & Safety Complete Masterclass
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.
#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;
}
#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;
}
#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;
}
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().
#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;
}
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.