Tutorials Logic, IN +91 8092939553 info@tutorialslogic.com
Navigation
Home About Us Contact Us Blogs FAQs
Tutorials
All Tutorials
Services
Academic Projects Resume Writing Interview Questions Website Development
Compiler Tutorials

C++ Exception Handling — try, throw, catch | Tutorials Logic

What is Exception Handling?

Exception handling lets you deal with runtime errors gracefully - instead of crashing, the program can catch the error, handle it, and continue or exit cleanly. C++ uses three keywords:

  • try - wraps code that might throw an exception.
  • throw - signals that an error has occurred.
  • catch - handles the exception.
try / throw / catch Basics
#include <iostream>
#include <stdexcept>
using namespace std;

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

int main() {
    // Basic try-catch
    try {
        cout << divide(10, 2) << endl;  // 5
        cout << divide(10, 0) << endl;  // throws!
    } catch (const runtime_error &e) {
        cout << "Caught: " << e.what() << endl;
    }

    // Multiple catch blocks
    try {
        int arr[5] = {1, 2, 3, 4, 5};
        // vector::at() throws std::out_of_range
        vector<int> v = {1, 2, 3};
        cout << v.at(10) << endl;  // throws out_of_range
    } catch (const out_of_range &e) {
        cout << "Out of range: " << e.what() << endl;
    } catch (const exception &e) {
        cout << "General exception: " << e.what() << endl;
    } catch (...) {
        cout << "Unknown exception" << endl;
    }

    cout << "Program continues after exception" << endl;
    return 0;
}

Standard Exception Hierarchy

Exception ClassHeaderWhen thrown
std::exception<exception>Base class for all standard exceptions
std::runtime_error<stdexcept>Runtime errors (logic errors detected at runtime)
std::logic_error<stdexcept>Violations of logical preconditions
std::out_of_range<stdexcept>Index out of bounds (e.g., vector::at())
std::invalid_argument<stdexcept>Invalid argument passed to function
std::overflow_error<stdexcept>Arithmetic overflow
std::bad_alloc<new>Memory allocation failure (new)
Custom Exception Classes
#include <iostream>
#include <stdexcept>
using namespace std;

// Custom exception - inherit from std::exception
class InsufficientFundsException : public exception {
    double amount;
public:
    InsufficientFundsException(double amount) : amount(amount) {}

    const char* what() const noexcept override {
        return "Insufficient funds";
    }

    double getAmount() const { return amount; }
};

class BankAccount {
    double balance;
public:
    BankAccount(double b) : balance(b) {}

    void withdraw(double amount) {
        if (amount > balance)
            throw InsufficientFundsException(amount - balance);
        balance -= amount;
    }

    double getBalance() const { return balance; }
};

int main() {
    BankAccount acc(100.0);

    try {
        acc.withdraw(50.0);
        cout << "Balance: $" << acc.getBalance() << endl;  // $50

        acc.withdraw(200.0);  // throws!
    } catch (const InsufficientFundsException &e) {
        cout << "Error: " << e.what() << endl;
        cout << "Short by: $" << e.getAmount() << endl;
    }

    return 0;
}

noexcept — Declaring No-throw Functions

noexcept (C++11) tells the compiler that a function will never throw an exception. This enables optimisations and is required for move constructors/operators to work efficiently with STL containers. If a noexcept function does throw, std::terminate() is called immediately.

noexcept Specifier
#include <iostream>
#include <stdexcept>
using namespace std;

// noexcept — guaranteed not to throw
int add(int a, int b) noexcept {
    return a + b;
}

// noexcept(false) — may throw (default for most functions)
double divide(double a, double b) noexcept(false) {
    if (b == 0) throw runtime_error("Division by zero");
    return a / b;
}

// Conditional noexcept — noexcept if T's operations are noexcept
template <typename T>
void swap(T &a, T &b) noexcept(noexcept(T(move(a)))) {
    T temp = move(a);
    a = move(b);
    b = move(temp);
}

class MyClass {
public:
    // Move constructor should be noexcept for STL efficiency
    MyClass(MyClass &&other) noexcept {
        // transfer resources
    }

    // Destructor should always be noexcept (it is by default)
    ~MyClass() noexcept = default;
};

int main() {
    cout << add(3, 4) << endl;  // 7

    // Check at compile time
    cout << boolalpha;
    cout << "add is noexcept: "    << noexcept(add(1, 2))    << endl;  // true
    cout << "divide is noexcept: " << noexcept(divide(1, 2)) << endl;  // false

    return 0;
}

RAII — Resource Acquisition Is Initialization

RAII is the most important C++ idiom. The idea: tie resource lifetime to object lifetime. Acquire a resource in the constructor, release it in the destructor. When the object goes out of scope, the destructor runs automatically — even if an exception is thrown. This eliminates resource leaks without needing finally blocks.

RAII — Automatic Resource Management
#include <iostream>
#include <memory>
using namespace std;

// RAII wrapper for a raw pointer
class ManagedArray {
    int *data;
    size_t size;
public:
    ManagedArray(size_t n) : size(n), data(new int[n]) {
        cout << "Allocated " << n << " ints" << endl;
    }

    ~ManagedArray() {
        delete[] data;  // always runs — even if exception thrown
        cout << "Freed memory" << endl;
    }

    int &operator[](size_t i) { return data[i]; }
};

void riskyFunction() {
    ManagedArray arr(10);  // resource acquired
    arr[0] = 42;

    throw runtime_error("Something went wrong!");
    // arr destructor runs here automatically — no leak!
}

int main() {
    try {
        riskyFunction();
    } catch (const exception &e) {
        cout << "Caught: " << e.what() << endl;
    }
    // "Freed memory" was printed before catch — RAII works!

    // Modern C++ RAII: smart pointers
    {
        auto p = make_unique<int[]>(100);  // allocated
        p[0] = 99;
    }  // automatically freed here — no delete needed

    return 0;
}
#include <iostream>
#include <fstream>
#include <stdexcept>
using namespace std;

// RAII file wrapper — file always closed even on exception
class FileGuard {
    ofstream file;
public:
    FileGuard(const string &path) {
        file.open(path);
        if (!file.is_open())
            throw runtime_error("Cannot open: " + path);
        cout << "File opened" << endl;
    }

    ~FileGuard() {
        if (file.is_open()) {
            file.close();
            cout << "File closed (RAII)" << endl;
        }
    }

    void write(const string &line) { file << line << "\n"; }
};

void writeData() {
    FileGuard fg("output.txt");
    fg.write("Line 1");
    fg.write("Line 2");
    throw runtime_error("Simulated error");
    fg.write("Line 3");  // never reached
}   // fg destructor runs here — file is closed!

int main() {
    try {
        writeData();
    } catch (const exception &e) {
        cout << "Error: " << e.what() << endl;
    }
    // File was properly closed despite the exception
    return 0;
}

Stack Unwinding

When an exception is thrown, C++ unwinds the call stack — it destroys all local objects in reverse order of construction until it finds a matching catch block. This is why RAII works: destructors are guaranteed to run during unwinding.

Stack Unwinding in Action
#include <iostream>
using namespace std;

struct Guard {
    string name;
    Guard(string n) : name(n) { cout << name << " created\n"; }
    ~Guard()                  { cout << name << " destroyed\n"; }
};

void c() {
    Guard g3("g3");
    throw runtime_error("Error in c()");
}

void b() {
    Guard g2("g2");
    c();  // exception propagates up
}

void a() {
    Guard g1("g1");
    b();
}

int main() {
    try {
        a();
    } catch (const exception &e) {
        cout << "Caught: " << e.what() << endl;
    }
    return 0;
}

/*
Output:
g1 created
g2 created
g3 created
g3 destroyed   <-- unwinding: innermost first
g2 destroyed
g1 destroyed
Caught: Error in c()
*/

Exception Handling Best Practices

DoDon't
Catch by const reference: catch(const exception &e)Catch by value — causes slicing
Use standard exception types (runtime_error, logic_error)Throw raw integers or strings
Add virtual ~Base() to exception base classesThrow from destructors — causes terminate()
Use RAII to manage resources — no manual cleanup neededUse bare try/catch as a substitute for RAII
Mark non-throwing functions noexceptCatch ... and silently swallow exceptions
Let exceptions propagate to the right levelUse exceptions for normal control flow
Document what exceptions a function may throwThrow from constructors without cleanup

Ready to Level Up Your Skills?

Explore 500+ free tutorials across 20+ languages and frameworks.