C++ Exception Handling: try-catch, Custom Exceptions, noexcept, std::expected & DSA (2025)
⚡ Quick Answer: Exception Handling in C++
- throw — signals an error; try — wraps risky code; catch — handles the error
- Always catch by const reference:
catch (const std::exception& e) - Standard hierarchy:
std::exception→runtime_error,out_of_range,invalid_argument, etc. - Custom exceptions: inherit from
std::exception, overridewhat() - noexcept: promise function won't throw; required on move ops and destructors
- Exception safety: nothrow > strong > basic — aim for the strongest practical
- RAII: destructors automatically clean up — no manual cleanup in catch blocks needed
Exception handling is how C++ separates error detection from error recovery — keeping your
normal code path clean while guaranteeing resources are freed even when things go wrong.
This guide covers everything: the mechanics, the full standard exception hierarchy, custom
exception design, exception safety guarantees, noexcept optimization, modern
alternatives like std::expected, and five complete DSA examples.
try / catch / throw
Core mechanics
Exception Hierarchy
All standard types
Custom Exceptions
Class hierarchy design
noexcept
Optimization & safety
Exception Safety
nothrow/strong/basic
std::expected
C++23 alternative
DSA Examples
Stack, Tree, Graph
FAQ / Interview
Top 10 questions
try / catch / throw — Core Mechanics
#include <iostream>
#include <stdexcept>
#include <string>
using namespace std;
// Function that throws on bad input
double safeDivide(double a, double b) {
if (b == 0.0)
throw runtime_error("Division by zero"); // ① throw
return a / b;
}
int main() {
// ── Multiple catch blocks — ordered specific → general ──────
try { // ② try
cout << safeDivide(10, 0) << endl;
}
catch (const runtime_error& e) { // ③ catch specific
cout << "Runtime error: " << e.what() << endl;
}
catch (const exception& e) { // catches ANY std exception
cout << "General error: " << e.what() << endl;
}
catch (...) { // last resort — catches anything
cout << "Unknown error" << endl;
}
// ── Normal case — no exception thrown ─────────────────────
try {
cout << "Result: " << safeDivide(10, 2) << endl;
}
catch (const runtime_error& e) {
cout << "Error: " << e.what() << endl;
}
// ── Stack unwinding: RAII cleanup runs automatically ───────
try {
auto f = make_unique<int>(42); // allocated
throw runtime_error("mid-operation failure");
// unique_ptr destructor runs during unwinding — no leak!
}
catch (const exception& e) {
cout << "Caught & cleaned up: " << e.what() << endl;
}
return 0;
}
Runtime error: Division by zero
Result: 5
Caught & cleaned up: mid-operation failurecatch (const std::exception& e)
before catch (const std::runtime_error& e), the runtime_error block can never be
reached — the general exception catches it first.
Standard Exception Hierarchy — All Types Explained
C++ Standard Exception Hierarchy (<exception> + <stdexcept>)
| Exception type | Header | When to throw |
|---|---|---|
| std::invalid_argument | <stdexcept> | Function argument value is invalid (negative size, null where disallowed) |
| std::out_of_range | <stdexcept> | Index/value outside valid range — vector::at, string::at throw this |
| std::runtime_error | <stdexcept> | General runtime failure (file not found, parse error, network failure) |
| std::overflow_error | <stdexcept> | Stack overflow, numeric overflow |
| std::underflow_error | <stdexcept> | Stack underflow, numeric underflow |
| std::bad_alloc | <new> | Thrown by new when memory allocation fails |
| std::bad_cast | <typeinfo> | dynamic_cast to reference on incompatible type |
| std::system_error | <system_error> | OS-level errors (filesystem, sockets) — has error_code |
Custom Exception Classes — Hierarchy Design
#include <iostream>
#include <stdexcept>
#include <string>
using namespace std;
// ── Application-level base exception ──────────────────────────
class AppException : public runtime_error {
public:
explicit AppException(const string& msg)
: runtime_error("[AppError] " + msg) {}
};
// ── Domain-specific exceptions ─────────────────────────────────
class DatabaseException : public AppException {
public:
explicit DatabaseException(const string& msg)
: AppException("DB: " + msg) {}
};
class NetworkException : public AppException {
int code_;
public:
NetworkException(const string& msg, int code)
: AppException("Net[" + to_string(code) + "]: " + msg), code_(code) {}
int statusCode() const noexcept { return code_; }
};
// ── Functions that throw domain exceptions ─────────────────────
void connectDB(const string& dsn) {
if (dsn.empty())
throw DatabaseException("Empty connection string");
}
void fetchURL(const string& url) {
if (url.find("https") == string::npos)
throw NetworkException("HTTPS required for: " + url, 403);
}
int main() {
// Catch specific type
try { connectDB(""); }
catch (const DatabaseException& e) {
cout << "DB caught: " << e.what() << endl;
}
// Catch with extra data
try { fetchURL("http://example.com"); }
catch (const NetworkException& e) {
cout << "Net caught: " << e.what()
<< " (code " << e.statusCode() << ")" << endl;
}
// Catch at base level — catches all AppExceptions via polymorphism
try {
connectDB("");
}
catch (const AppException& e) {
cout << "App-level handler: " << e.what() << endl;
}
return 0;
}
DB caught: [AppError] DB: Empty connection string
Net caught: [AppError] Net[403]: HTTPS required for: http://example.com (code 403)
App-level handler: [AppError] DB: Empty connection stringAppException) which itself derives from a standard exception. This lets
callers choose their granularity: catch the specific type for targeted handling, catch the
domain base for any app error, or catch std::exception as the last resort.
noexcept — Optimization & Destructor Safety
#include <iostream>
#include <vector>
#include <stdexcept>
using namespace std;
class Buffer {
int* data;
int n;
public:
Buffer(int size) : n(size), data(new int[size]()) {}
// Move constructor — noexcept REQUIRED for vector to use it
Buffer(Buffer&& o) noexcept : n(o.n), data(o.data) {
o.data = nullptr; o.n = 0;
}
// Destructor — implicitly noexcept since C++11
~Buffer() { delete[] data; } // NEVER throw here
// Simple getter — logically can't fail → noexcept
int size() const noexcept { return n; }
// Conditional noexcept: noexcept if index operator is noexcept
int& operator[](int i) noexcept { return data[i]; } // no bounds check
int& at(int i) { // bounds check — CAN throw
if (i < 0 || i >= n) throw out_of_range("Buffer::at: index " + to_string(i));
return data[i];
}
};
// Check if noexcept propagates correctly in templates
static_assert(noexcept(Buffer(Buffer())), "Move must be noexcept for vector");
int main() {
vector<Buffer> vec;
vec.reserve(3);
for (int i=1; i<=3; i++) vec.emplace_back(i*100);
// vector uses Buffer's noexcept move during reallocation ✓
try {
vec[0].at(200); // throws out_of_range
} catch (const out_of_range& e) {
cout << "Caught: " << e.what() << endl;
}
return 0;
}
Caught: Buffer::at: index 200Exception Safety Guarantees
noexcept. Required for: move constructors, destructors, swap functions. Enables std::vector move optimization.#include <algorithm> // std::swap
using namespace std;
class SafeArray {
int* data;
int size;
public:
SafeArray(int n) : size(n), data(new int[n]()) {}
~SafeArray() { delete[] data; }
// Copy constructor: might throw if new fails
SafeArray(const SafeArray& o) : size(o.size), data(new int[o.size]) {
copy(o.data, o.data + size, data);
}
// swap is noexcept — just swaps pointers
friend void swap(SafeArray& a, SafeArray& b) noexcept {
using std::swap;
swap(a.data, b.data);
swap(a.size, b.size);
}
// Copy-and-swap: STRONG guarantee
// 'other' is constructed by copy ctor — if that throws, *this unchanged
SafeArray& operator=(SafeArray other) noexcept { // pass by value = copy
swap(*this, other); // noexcept swap — can't fail
return *this; // 'other' destroyed with old data
}
};
std::expected (C++23) — Modern Alternative to Exceptions
#include <expected> // C++23
#include <iostream>
#include <string>
using namespace std;
// Return type explicitly shows success OR error — no exceptions needed
expected<double, string> safeDivide(double a, double b) {
if (b == 0.0) return unexpected("Division by zero");
return a / b; // success path
}
expected<int, string> parseAge(string_view s) {
if (s.empty()) return unexpected("Empty string");
try { return stoi(string(s)); }
catch (...) { return unexpected("Not a number: " + string(s)); }
}
int main() {
// Check if result or error
auto result = safeDivide(10.0, 3.0);
if (result) cout << "Result: " << *result << endl; // 3.333
else cout << "Error: " << result.error() << endl;
auto bad = safeDivide(10.0, 0.0);
cout << bad.value_or(-1.0) << endl; // -1 (default on error)
// Monadic chaining (C++23)
auto age = parseAge("25")
.and_then([](int a) -> expected<int, string> {
if (a < 0 || a > 150) return unexpected("Age out of range");
return a;
});
if (age) cout << "Age: " << *age << endl;
return 0;
}
Result: 3.33333
-1
Age: 25std::expected when
failure is a normal, expected outcome (parsing, searching, validation) and you want zero
overhead and an explicit error type in the signature. Use exceptions when failure is
unexpected or exceptional (out of memory, file corruption, hardware failure) and you want
errors to propagate automatically without every caller checking a return value.
DSA Examples: Stack, BST, Graph
1. Generic Stack with overflow/underflow
#include <iostream>
#include <stdexcept>
#include <array>
using namespace std;
template<typename T, int MAX = 5>
class Stack {
array<T, MAX> arr;
int top = -1;
public:
void push(const T& v) {
if (top == MAX-1) throw overflow_error("Stack overflow (max=" + to_string(MAX) + ")");
arr[++top] = v;
}
T pop() {
if (top < 0) throw underflow_error("Stack underflow");
return arr[top--];
}
const T& peek() const {
if (top < 0) throw underflow_error("Peek on empty stack");
return arr[top];
}
bool empty() const noexcept { return top < 0; }
int size() const noexcept { return top + 1; }
};
int main() {
Stack<int, 3> s;
try {
s.push(1); s.push(2); s.push(3);
s.push(4); // throws overflow
} catch (const overflow_error& e) { cout << "Overflow: " << e.what() << endl; }
try {
while (!s.empty()) cout << s.pop() << " ";
s.pop(); // throws underflow
} catch (const underflow_error& e) { cout << "\nUnderflow: " << e.what() << endl; }
}
Overflow: Stack overflow (max=3)
3 2 1
Underflow: Stack underflow2. BST with invalid input protection
#include <iostream>
#include <memory>
#include <stdexcept>
using namespace std;
struct BST {
int val;
unique_ptr<BST> left, right;
explicit BST(int v) : val(v) {}
};
unique_ptr<BST> insert(unique_ptr<BST> root, int v) {
if (v == INT_MIN || v == INT_MAX)
throw invalid_argument("Reserved sentinel values not allowed");
if (!root) return make_unique<BST>(v);
if (v < root->val) root->left = insert(move(root->left), v);
else root->right = insert(move(root->right), v);
return root;
}
int find(const BST* root, int target) {
if (!root) throw runtime_error("Value " + to_string(target) + " not found in BST");
if (target == root->val) return root->val;
return target < root->val ? find(root->left.get(), target)
: find(root->right.get(), target);
}
int main() {
try {
auto tree = insert(nullptr, INT_MIN); // throws
} catch (const invalid_argument& e) { cout << "Invalid: " << e.what() << endl; }
unique_ptr<BST> tree;
for (int v : {5,3,7}) tree = insert(move(tree), v);
try {
cout << "Found: " << find(tree.get(), 99) << endl; // throws
} catch (const runtime_error& e) { cout << "Not found: " << e.what() << endl; }
return 0;
}
Invalid: Reserved sentinel values not allowed
Not found: Value 99 not found in BSTBest Practices & Common Mistakes
✅ Throw by value, catch by const ref
throw runtime_error("msg"); and catch (const std::exception& e). Never catch by value — it slices the exception.
✅ Derive custom exceptions from std::exception
Inherit from std::runtime_error or std::logic_error for the right category. Gives callers a clean catch hierarchy.
✅ Use RAII — not try/finally
C++ has no finally block. Use RAII types (unique_ptr, fstream) so destructors clean up — even during exceptions.
✅ noexcept on move ops and destructors
Move constructors, move assignment, and destructors must be noexcept for correct std::vector behavior and safe stack unwinding.
✅ Provide meaningful what() messages
Include the value that caused the error: "Index 42 out of range [0, 10)". Useless messages like "error" waste debugging time.
✅ Rethrow with bare throw;
throw; preserves the dynamic type. throw e; slices to the caught type — always a bug when rethrowing.
❌ Never throw from destructors
If an exception propagates during stack unwinding and a destructor throws, std::terminate() is called. Wrap cleanup in try/catch inside the destructor.
❌ Don't use exceptions for normal flow
If an item is "not found" regularly, return std::optional or std::expected — don't throw. Exceptions are for truly exceptional conditions.
FAQ / Interview Questions
const reference: catch (const std::exception& e). Catching by value copies the exception object and slices it — if the thrown type is a derived class, the copy loses derived members and the virtual what() returns the base class message. Catching by pointer is also wrong — you'd be responsible for deleting it. The only correct form: catch (const ExceptionType& e).std::terminate() immediately, killing the program. Since C++11, destructors are implicitly noexcept. Rule: any cleanup that can fail should be wrapped in a try/catch inside the destructor — catch it, log it, and swallow it rather than letting it escape.noexcept. Required for destructors, move constructors, move assignment. Strong: if the function throws, the state is exactly as if the call never happened (commit or rollback). Achieved with copy-and-swap. Basic: if the function throws, the program is in a valid state and no resources are leaked — but the specific state may have changed. This is the minimum acceptable level. Aim for the strongest guarantee practical — nothrow for moves/dtors, strong for state-mutating operations.std::expected<T,E> (C++23) represents either a result value or an error value in the same return type — no exception overhead. Use it when: failures are expected and frequent (parsing, validation, lookup), you want zero overhead on the success path, or you want the error type explicit in the function signature. Use exceptions when: failures are truly exceptional (out of memory, hardware failure), or errors need to propagate through many layers automatically without each intermediate function checking a return value.throw; with no argument inside a catch block: catch (const std::exception& e) { log(e.what()); throw; }. This rethrows the original exception preserving its dynamic type and original message. Never write throw e; — this creates a new exception by copying e, sliced to the declared catch type (losing any derived class information). Bare throw; is only valid inside a catch block or a function called from one.Write exception-safe C++ from day one
Structured lessons, 200+ exercises, completion certificate. Join 50,000+ students on CoodeVerse.