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.
#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 Class | Header | When 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) |
#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.
#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.
#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.
#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
| Do | Don'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 classes | Throw from destructors — causes terminate() |
| Use RAII to manage resources — no manual cleanup needed | Use bare try/catch as a substitute for RAII |
Mark non-throwing functions noexcept | Catch ... and silently swallow exceptions |
| Let exceptions propagate to the right level | Use exceptions for normal control flow |
| Document what exceptions a function may throw | Throw from constructors without cleanup |
Level Up Your C plus plus Skills
Master C plus plus with these hand-picked resources