Beyond the Basics
Exception Handling
Handle errors cleanly with try, throw and catch: standard exception classes, multiple and catch-all handlers, custom exceptions, and stack unwinding.
Programs meet errors at run time: division by zero, a missing file, bad input, memory exhaustion. Checking return codes everywhere clutters code and errors are easy to ignore. Exceptions separate error detection from error handling.
throw— signal an error.try— wrap code that might throw.catch— handle a specific kind of error.
#include <iostream>
using namespace std;
double divide(double a, double b) {
if (b == 0) throw "Division by zero!"; // throw a value
return a / b;
}
int main() {
try {
cout << divide(10, 4) << endl;
cout << divide(1, 0) << endl; // throws: the rest of try is skipped
cout << "never printed" << endl;
} catch (const char *msg) {
cout << "Error: " << msg << endl;
}
cout << "Program continues normally." << endl;
return 0;
}2.5 Error: Division by zero! Program continues normally.
Standard exception classes
The standard library throws objects derived from std::exception. Each has a what() method returning a message.
Class (header <stdexcept>) |
Typical cause |
|---|---|
invalid_argument |
a bad argument, e.g. stoi("abc") |
out_of_range |
index past the end, e.g. vector::at(100) |
length_error |
a container grown beyond its maximum |
overflow_error, underflow_error |
arithmetic range errors |
runtime_error |
general errors detectable only at run time |
bad_alloc (<new>) |
new failed to get memory |
#include <iostream>
#include <vector>
#include <string>
#include <stdexcept>
using namespace std;
int main() {
vector<int> v = {1, 2, 3};
try {
cout << v.at(1) << endl;
cout << v.at(10) << endl; // .at() checks bounds and throws
} catch (const out_of_range &e) {
cout << "out_of_range caught" << endl;
}
string inputs[] = {"42", "abc", "99999999999"};
for (const string &s : inputs) {
try {
int n = stoi(s); // may throw
cout << "stoi(\"" << s << "\") = " << n << endl;
} catch (const invalid_argument &) {
cout << "\"" << s << "\" is not a number" << endl;
} catch (const out_of_range &) {
cout << "\"" << s << "\" is too big for an int" << endl;
}
}
return 0;
}2
out_of_range caught
stoi("42") = 42
"abc" is not a number
"99999999999" is too big for an intMultiple catch blocks and catch-all
Handlers are tried in order; the first matching one runs. Put specific types before general ones, and catch (...) (catch anything) last:
#include <iostream>
#include <stdexcept>
using namespace std;
void risky(int option) {
if (option == 1) throw 404;
if (option == 2) throw runtime_error("disk not ready");
if (option == 3) throw 3.14;
cout << "option " << option << " ran fine" << endl;
}
int main() {
for (int option = 0; option <= 3; option++) {
try {
risky(option);
} catch (int code) {
cout << "int error code " << code << endl;
} catch (const exception &e) { // catches every std exception
cout << "std::exception: " << e.what() << endl;
} catch (...) {
cout << "unknown error" << endl;
}
}
return 0;
}option 0 ran fine int error code 404 std::exception: disk not ready unknown error
Custom exceptions
Derive from std::exception (or runtime_error) to describe your own errors:
#include <iostream>
#include <stdexcept>
#include <string>
using namespace std;
class InsufficientFunds : public runtime_error {
public:
double shortBy;
InsufficientFunds(double s) : runtime_error("insufficient funds"), shortBy(s) {}
};
class Account {
double balance = 1000;
public:
void withdraw(double amt) {
if (amt <= 0) throw invalid_argument("amount must be positive");
if (amt > balance) throw InsufficientFunds(amt - balance);
balance -= amt;
}
double getBalance() const { return balance; }
};
int main() {
Account acc;
double amt;
cout << "Withdraw how much? ";
cin >> amt;
try {
acc.withdraw(amt);
cout << "Done. Balance: " << acc.getBalance() << endl;
} catch (const InsufficientFunds &e) {
cout << "Failed: " << e.what() << " (short by Rs " << e.shortBy << ")" << endl;
} catch (const invalid_argument &e) {
cout << "Failed: " << e.what() << endl;
}
return 0;
}Withdraw how much? 2500
Failed: insufficient funds (short by Rs 1500)Stack unwinding
When an exception propagates out of functions, every local object on the way is destroyed properly — its destructor runs. This is why C++ resources should be owned by objects (RAII): cleanup happens automatically even on errors.
#include <iostream>
#include <stdexcept>
using namespace std;
struct Guard {
const char *name;
Guard(const char *n) : name(n) { cout << "acquire " << name << endl; }
~Guard() { cout << "release " << name << endl; }
};
void level2() { Guard g("file"); throw runtime_error("parse failed"); }
void level1() { Guard g("lock"); level2(); cout << "not reached" << endl; }
int main() {
try { level1(); }
catch (const exception &e) { cout << "caught: " << e.what() << endl; }
return 0;
}acquire lock acquire file release file release lock caught: parse failed
Test yourself
4 questions · pick an answer to check it instantly
- 1
What happens to the remaining statements in a
tryblock after an exception is thrown?Throwing transfers control immediately to the nearest matching handler. - 2
Where should
catch (...)appear?It matches everything, so any handler after it would never run. - 3
Which exception does
vector::at()throw for a bad index?at()checks bounds;operator[]does not. - 4
During stack unwinding, what happens to local objects in the functions being exited?
Unwinding destroys each local object, which is why RAII cleanup is reliable.