Last updated: December 2025

C++ Exception Handling: try-catch, Custom Exceptions, noexcept, std::expected & DSA (2025)

By CoodeVerse Editorial Team ✓ 2025 Verified ⏱ 20 min read 🎯 Beginner–Intermediate 📦 C++11/17/23
Difficulty:
Beginner–Intermediate — Prerequisites: Classes & Objects, Constructors & Destructors

⚡ Quick Answer: Exception Handling in C++

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

🔧 Section 1

try / catch / throw — Core Mechanics

exception_basics.cpp — all core mechanics in one fileC++
#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;
}
Output
Runtime error: Division by zero
Result: 5
Caught & cleaned up: mid-operation failure
Order catch blocks from most specific to most general. C++ tests catch blocks top-to-bottom and uses the FIRST matching one. If you put catch (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.
🌳 Section 2

Standard Exception Hierarchy — All Types Explained

C++ Standard Exception Hierarchy (<exception> + <stdexcept>)

std::exceptionbase — virtual what() const noexcept
├─ std::logic_errorbugs detectable before runtime
│ ├─ invalid_argumentargument value not accepted
│ ├─ domain_errormath domain violation
│ ├─ length_errorexceeds max allowed length
│ └─ out_of_rangeindex/value outside valid range
├─ std::runtime_errordetectable only at runtime
│ ├─ range_errorcomputed result out of range
│ ├─ overflow_errorarithmetic overflow
│ └─ underflow_errorarithmetic underflow
├─ std::bad_allocnew[] fails — out of memory
├─ std::bad_castdynamic_cast to reference fails
├─ std::bad_typeidtypeid on null pointer
├─ std::system_errorOS/system call failure (C++11)
└─ std::ios_base::failurestream I/O error
Exception typeHeaderWhen 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
🏗️ Section 3

Custom Exception Classes — Hierarchy Design

custom_exceptions.cpp — production-quality exception hierarchyC++
#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;
}
Output
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 string
Design custom exceptions in a hierarchy. Derive from a domain base class (e.g., AppException) 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.
⚡ Section 4

noexcept — Optimization & Destructor Safety

noexcept_demo.cpp — when noexcept mattersC++
#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;
}
Output
Caught: Buffer::at: index 200
🛡️ Section 5

Exception Safety Guarantees

✅ Nothrow (best)
The function never throws. Mark with noexcept. Required for: move constructors, destructors, swap functions. Enables std::vector move optimization.
💙 Strong
If it throws, the object/state is exactly as it was before the call — commit or rollback. Technique: copy-and-swap idiom. std::vector::push_back provides strong guarantee.
⚠️ Basic (minimum)
If it throws, the program is in a valid (but unspecified) state and no resources are leaked. All invariants hold. This is the minimum acceptable level for production code.
copy_and_swap.cpp — strong exception guaranteeC++
#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
    }
};
✨ Section 6

std::expected (C++23) — Modern Alternative to Exceptions

std_expected.cpp — type-safe error handling without exceptionsC++23
#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;
}
Output
Result: 3.33333
-1
Age: 25
When to use std::expected vs exceptions: Use std::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.
🏆 Section 7

DSA Examples: Stack, BST, Graph

1. Generic Stack with overflow/underflow

stack_exceptions.cppC++
#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; }
}
Output
Overflow: Stack overflow (max=3)
3 2 1
Underflow: Stack underflow

2. BST with invalid input protection

bst_exceptions.cpp — null safety + range checkingC++
#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;
}
Output
Invalid: Reserved sentinel values not allowed
Not found: Value 99 not found in BST

Best 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

Always catch by 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).
When an exception is thrown, C++ walks back up the call stack frame by frame looking for a matching catch block. At each frame it exits, the destructors of all local objects in that frame run automatically. This is stack unwinding. Combined with RAII, stack unwinding guarantees that all resources (heap memory, file handles, locks) are released even when an exception propagates — without any explicit cleanup code in catch blocks.
When an exception propagates during stack unwinding, destructors of local objects run. If one of those destructors throws another exception, C++ has two simultaneously active exceptions — it cannot handle both and calls 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.
Nothrow: the function never throws — mark with 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.
Use bare 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.

Related C++ Topics on CoodeVerse

Constructors & Destructors Classes & Objects Smart Pointers & RAII Debugging & Optimization Design Patterns Deployment & Best Practices Templates 📚 Full C++ Reading Materials

CoodeVerse Editorial Team

Senior C++ engineers and CS educators. All examples compiled with GCC 13 and Clang 16, -Wall -Wextra -std=c++17.