🛡️ C++ Exceptions: Your Safety Net for Unexpected Problems
The Big Picture: What Are Exceptions?
Imagine you’re walking on a tightrope. Below you is a safety net. If you slip (something goes wrong), the net catches you so you don’t crash to the ground.
Exceptions in C++ work exactly like that safety net!
When your program does something risky—like dividing by zero, opening a missing file, or running out of memory—instead of crashing, it can “throw” a problem (exception). Somewhere else in your code, a “catch” block is waiting like a safety net to handle it gracefully.
🎯 What You’ll Learn
graph TD A["🎯 Exceptions"] --> B["try-catch Blocks"] A --> C["throw Statement"] A --> D["Standard Exception Classes"] A --> E["Custom Exceptions"] A --> F["noexcept Specification"] A --> G["Stack Unwinding"] A --> H["Exception Safety Guarantees"]
1️⃣ try-catch Blocks: Setting Up Your Safety Net
Think of try as saying “I’m about to do something risky” and catch as “If something goes wrong, here’s what to do.”
The Basic Pattern
try {
// Risky code goes here
// Like walking on a tightrope
} catch (exception_type& e) {
// Handle the problem here
// The safety net catches you!
}
Real Example: Dividing Numbers
#include <iostream>
#include <stdexcept>
using namespace std;
int divide(int a, int b) {
if (b == 0) {
throw runtime_error("Can't divide by zero!");
}
return a / b;
}
int main() {
try {
cout << divide(10, 2) << endl; // Works: 5
cout << divide(10, 0) << endl; // Problem!
} catch (runtime_error& e) {
cout << "Oops! " << e.what() << endl;
}
return 0;
}
Output:
5
Oops! Can't divide by zero!
Catching Multiple Types
You can have multiple safety nets for different problems:
try {
// risky code
} catch (runtime_error& e) {
cout << "Runtime problem: " << e.what();
} catch (logic_error& e) {
cout << "Logic problem: " << e.what();
} catch (...) {
// This catches EVERYTHING else
cout << "Unknown problem occurred!";
}
💡 Pro Tip: The
catch(...)is like a giant safety net that catches anything. Use it as a last resort!
2️⃣ throw Statement: Sounding the Alarm
When you detect a problem, you throw an exception. It’s like yelling “HELP!” and trusting someone will catch you.
Simple Throw
throw runtime_error("Something broke!");
Throwing Different Things
// Throw a standard exception
throw invalid_argument("Age can't be negative");
// Throw a number (not recommended, but possible)
throw 404;
// Throw a string (also not recommended)
throw "File not found";
When Should You Throw?
✅ Good reasons to throw:
- Can’t complete the task you were asked to do
- Input is invalid or unexpected
- A critical resource is unavailable
❌ Bad reasons to throw:
- Normal conditions (like end of a list)
- Things you could handle locally
Example: Validating Input
void setAge(int age) {
if (age < 0) {
throw invalid_argument("Age can't be negative!");
}
if (age > 150) {
throw out_of_range("Age seems unrealistic!");
}
this->age = age;
}
3️⃣ Standard Exception Classes: Your Ready-Made Safety Nets
C++ comes with a family of exception types ready to use. They all live in <stdexcept>.
graph TD A["exception"] --> B["logic_error"] A --> C["runtime_error"] B --> D["invalid_argument"] B --> E["domain_error"] B --> F["length_error"] B --> G["out_of_range"] C --> H["range_error"] C --> I["overflow_error"] C --> J["underflow_error"]
The Most Useful Ones
| Exception | When to Use | Example |
|---|---|---|
runtime_error |
Something failed at runtime | File not found |
invalid_argument |
Bad input value | Negative age |
out_of_range |
Index too big/small | Array index 100 in size-10 array |
logic_error |
Bug in program logic | Called function in wrong order |
overflow_error |
Number too big | Adding huge numbers |
Using Standard Exceptions
#include <stdexcept>
#include <vector>
int getElement(vector<int>& v, int index) {
if (index < 0 || index >= v.size()) {
throw out_of_range("Index is out of bounds!");
}
return v[index];
}
The .what() Method
Every standard exception has a what() method that tells you what went wrong:
try {
throw runtime_error("Disk is full!");
} catch (exception& e) {
cout << e.what(); // Prints: Disk is full!
}
4️⃣ Custom Exceptions: Building Your Own Safety Nets
Sometimes the standard exceptions don’t fit your needs. You can create your own!
Simple Custom Exception
class NegativeBalanceError : public exception {
public:
const char* what() const noexcept override {
return "Balance cannot be negative!";
}
};
Custom Exception with Details
class InsufficientFundsError : public runtime_error {
private:
double requested;
double available;
public:
InsufficientFundsError(double req, double avail)
: runtime_error("Insufficient funds"),
requested(req), available(avail) {}
double getRequested() const { return requested; }
double getAvailable() const { return available; }
};
Using Your Custom Exception
class BankAccount {
double balance = 100.0;
public:
void withdraw(double amount) {
if (amount > balance) {
throw InsufficientFundsError(amount, balance);
}
balance -= amount;
}
};
int main() {
BankAccount account;
try {
account.withdraw(500.0); // Too much!
} catch (InsufficientFundsError& e) {
cout << e.what() << endl;
cout << "You wanted: quot; << e.getRequested();
cout << ", You have: quot; << e.getAvailable();
}
}
5️⃣ noexcept: Promising Not to Throw
Sometimes you want to promise that a function will never throw an exception. That’s what noexcept does.
Why Use noexcept?
- Performance: The compiler can optimize better
- Clarity: Other programmers know it’s safe
- Required: Move constructors often need it
Adding noexcept
// This function promises: "I will NEVER throw!"
int add(int a, int b) noexcept {
return a + b;
}
// Conditional noexcept
template<typename T>
void swap(T& a, T& b) noexcept(noexcept(T(move(a)))) {
// noexcept if T's move constructor is noexcept
}
What Happens If You Break the Promise?
void dangerous() noexcept {
throw runtime_error("Oops!"); // BAD!
}
// If this throws, std::terminate() is called
// Your program CRASHES immediately!
⚠️ Warning: If a
noexceptfunction throws, your program terminates immediately! Only use it when you’re 100% sure.
Checking noexcept
cout << noexcept(add(1, 2)); // true
cout << noexcept(divide(1, 0)); // false
6️⃣ Stack Unwinding: How Exceptions Travel
When you throw an exception, C++ does something magical called stack unwinding. It’s like rewinding a video—going back through all the functions, cleaning up as it goes.
The Journey of an Exception
graph TD A["main calls functionA"] --> B["functionA calls functionB"] B --> C["functionB calls functionC"] C --> D["💥 functionC throws!"] D --> E["functionC destroyed/cleaned up"] E --> F["functionB destroyed/cleaned up"] F --> G["functionA destroyed/cleaned up"] G --> H["main catches exception"]
What Gets Cleaned Up?
During unwinding, destructors are called for all local objects:
class Logger {
public:
Logger(string name) : name(name) {
cout << "Creating " << name << endl;
}
~Logger() {
cout << "Destroying " << name << endl;
}
private:
string name;
};
void innerFunction() {
Logger log3("Inner");
throw runtime_error("Problem!");
}
void middleFunction() {
Logger log2("Middle");
innerFunction();
}
void outerFunction() {
Logger log1("Outer");
middleFunction();
}
int main() {
try {
outerFunction();
} catch (exception& e) {
cout << "Caught: " << e.what() << endl;
}
}
Output:
Creating Outer
Creating Middle
Creating Inner
Destroying Inner
Destroying Middle
Destroying Outer
Caught: Problem!
💡 Key Insight: Every object created before the throw gets properly destroyed! This is why RAII (Resource Acquisition Is Initialization) works so well in C++.
7️⃣ Exception Safety Guarantees: Promises About Behavior
When you write functions, you can make guarantees about what happens if an exception occurs.
The Three Levels
graph LR A["No Guarantee 💀"] --> B["Basic Guarantee ✅"] B --> C["Strong Guarantee 💪"] C --> D["No-Throw Guarantee 🛡️"]
1. Basic Guarantee (Minimum Standard)
Promise: “If I throw, your data won’t be corrupted, and no resources will leak.”
void basicSafe(vector<int>& v, int value) {
v.push_back(value); // Might throw
// If it throws, vector is still valid
// (maybe unchanged, maybe modified)
}
2. Strong Guarantee (Transactional)
Promise: “If I throw, everything stays exactly as it was before you called me.”
void strongSafe(vector<int>& v, int value) {
vector<int> temp = v; // Copy first
temp.push_back(value); // Modify copy
swap(v, temp); // Swap only if successful
// If push_back throws, original v unchanged!
}
3. No-Throw Guarantee (Bulletproof)
Promise: “I will NEVER throw an exception.”
void noThrowSafe(int& a, int& b) noexcept {
int temp = a;
a = b;
b = temp;
// Simple operations that can't fail
}
Real-World Example: Safe Assignment
class Document {
string* data;
public:
// Strong guarantee with copy-and-swap
Document& operator=(Document other) {
// other is a copy (passed by value)
swap(data, other.data); // noexcept
return *this;
// old data destroyed when other goes out of scope
}
};
Which Guarantee Should You Choose?
| Guarantee | When to Use |
|---|---|
| Basic | Minimum acceptable; always provide this |
| Strong | When rolling back is important |
| No-throw | Destructors, swap, move operations |
🎬 Putting It All Together
Here’s a complete example showing everything working together:
#include <iostream>
#include <stdexcept>
#include <string>
using namespace std;
// Custom exception
class WithdrawalError : public runtime_error {
double amount;
public:
WithdrawalError(double amt, const string& msg)
: runtime_error(msg), amount(amt) {}
double getAmount() const noexcept { return amount; }
};
class BankAccount {
double balance;
string owner;
public:
BankAccount(string name, double initial)
: owner(name), balance(initial) {}
// Strong guarantee
void withdraw(double amount) {
if (amount < 0) {
throw invalid_argument("Amount must be positive");
}
if (amount > balance) {
throw WithdrawalError(amount, "Insufficient funds");
}
balance -= amount; // Only happens if checks pass
}
// No-throw guarantee
double getBalance() const noexcept {
return balance;
}
};
int main() {
try {
BankAccount account("Alice", 100.0);
account.withdraw(30.0);
cout << "Balance: quot; << account.getBalance() << endl;
account.withdraw(200.0); // This will throw!
} catch (WithdrawalError& e) {
cout << "Error: " << e.what() << endl;
cout << "Tried to withdraw: quot; << e.getAmount() << endl;
} catch (invalid_argument& e) {
cout << "Invalid input: " << e.what() << endl;
} catch (...) {
cout << "Something unexpected happened!" << endl;
}
cout << "Program continues safely!" << endl;
return 0;
}
Output:
Balance: $70
Error: Insufficient funds
Tried to withdraw: $200
Program continues safely!
🎯 Quick Summary
| Concept | One-Liner |
|---|---|
| try-catch | Wrap risky code, handle problems gracefully |
| throw | Signal that something went wrong |
| Standard Exceptions | Ready-made exception types for common problems |
| Custom Exceptions | Create your own for specific needs |
| noexcept | Promise a function never throws |
| Stack Unwinding | Automatic cleanup when exceptions travel |
| Safety Guarantees | Promises about behavior during exceptions |
🚀 You Did It!
You now understand how C++ exceptions work—from throwing problems to catching them safely. Like a skilled tightrope walker, you know how to set up safety nets, handle falls gracefully, and keep your program running smoothly even when things go wrong.
Remember: Exceptions are for exceptional situations. Use them wisely, and your code will be both robust and elegant!
