4.2 Exception Handling
Why Exception Handling?
Traditional error handling in C uses return codes — every caller must check:
int result = openFile("data.txt");
if (result == ERROR_NOT_FOUND) { /* handle */ }
else if (result == ERROR_PERMISSION) { /* handle */ }
Problems:
- Forgot to check → silent bug
- Polluted main logic with error checks
- Hard to propagate errors up the call stack
- Constructor failure has no return value
Exception handling separates error reporting from error handling and supports automatic propagation up the call stack.
try {
openFile("data.txt");
} catch (const FileNotFoundException& e) {
cout << "File not found: " << e.what();
}
---
The Exception Handling Model
Three keywords:
| Keyword | Role |
|---|---|
try | Wraps code that might throw |
throw | Signals an exception (any type / value) |
catch | Handles an exception of a specific type |
Basic syntax
try {
// code that may throw
}
catch (Type1 e) {
// handle Type1
}
catch (Type2 e) {
// handle Type2
}
---
Example — Division by Zero
double divide(double a, double b) {
if (b == 0) throw "Division by zero!"; // throw a string literal
return a / b;
}
int main() {
try {
cout << divide(10, 2) << endl; // 5
cout << divide(5, 0) << endl; // throws
cout << "This won't print" << endl;
}
catch (const char* msg) {
cout << "Caught: " << msg << endl; // Division by zero!
}
cout << "Program continues" << endl;
return 0;
}
---
What Can Be Thrown?
In C++, you can throw any value of any type:
throw 42; // int
throw 3.14; // double
throw "Error!"; // const char*
throw string("Bad data"); // string object
throw MyException("foo"); // user-defined class
By convention, throw objects of exception classes (typically derived from std::exception).
---
std::exception Hierarchy
The C++ standard library defines an exception class hierarchy:
std::exception <exception>
│
├── logic_error <stdexcept>
│ ├── domain_error
│ ├── invalid_argument
│ ├── length_error
│ └── out_of_range
│
├── runtime_error
│ ├── overflow_error
│ ├── underflow_error
│ └── range_error
│
├── bad_alloc <new>
├── bad_cast <typeinfo>
├── bad_typeid
└── bad_exception
std::exception has a virtual what() method that returns a string description.
Using standard exceptions
#include <stdexcept>
void withdraw(double balance, double amount) {
if (amount < 0) throw invalid_argument("Negative amount");
if (amount > balance) throw runtime_error("Insufficient funds");
}
try {
withdraw(1000, 1500);
}
catch (const invalid_argument& e) {
cout << "Bad input: " << e.what();
}
catch (const runtime_error& e) {
cout << "Runtime: " << e.what();
}
---
Custom Exception Classes
The standard way is to derive from std::exception:
#include <exception>
class FileNotFoundException : public exception {
private:
string filename;
string message;
public:
FileNotFoundException(const string& fn) : filename(fn) {
message = "File not found: " + fn;
}
const char* what() const noexcept override {
return message.c_str();
}
const string& getFilename() const {
return filename;
}
};
void openFile(const string& fn) {
if (!fileExists(fn))
throw FileNotFoundException(fn);
}
try {
openFile("data.txt");
}
catch (const FileNotFoundException& e) {
cout << e.what() << endl;
cout << "Looking for: " << e.getFilename();
}
---
Catching Multiple Exception Types
try {
// ...
}
catch (const out_of_range& e) {
cout << "Out of range: " << e.what();
}
catch (const invalid_argument& e) {
cout << "Invalid arg: " << e.what();
}
catch (const exception& e) { // base — catches any std exception
cout << "Std exception: " << e.what();
}
catch (...) { // catch-all (generic catch)
cout << "Unknown exception";
}
Order matters!
Catch blocks are evaluated top to bottom. Catch derived types BEFORE base types:
catch (const exception& e) { /* would catch ALL — including derived */ }
catch (const out_of_range& e) { /* NEVER REACHED — unreachable */ }
Rule: Most-specific to most-general.
---
Generic Catch — catch(...)
catch(...) catches any exception regardless of type:
try {
riskyOperation();
}
catch (...) {
cout << "Something went wrong";
// No way to know what was thrown
}
Use sparingly — usually as a last-resort safety net.
---
Rethrowing Exceptions
Sometimes you want to partially handle an exception and rethrow for the caller:
void process() {
try {
riskyOp();
}
catch (const exception& e) {
cout << "Logging: " << e.what() << endl;
throw; // rethrow same exception
}
}
int main() {
try {
process();
}
catch (const exception& e) {
cout << "Final: " << e.what();
}
return 0;
}
throw; with no operand rethrows the current exception (works only inside a catch block).
You can also throw a different exception:
catch (const FileError& e) {
throw ApplicationError("File operation failed");
}
---
Exception Propagation (Stack Unwinding)
When an exception is thrown, the runtime:
- Locates the nearest enclosing
tryblock - Finds a matching
catchhandler - Unwinds the stack — destroys local objects in each function frame as it goes
- Executes the matching
catchbody
void func3() {
int x = 5;
throw runtime_error("oops");
// x destroyed during unwind
}
void func2() {
string s = "hello";
func3();
// s destroyed during unwind
}
void func1() {
Resource r;
try {
func2();
}
catch (const exception& e) {
cout << "Caught: " << e.what();
}
// r destroyed normally at end of func1
}
The destructor for each local object runs as the stack unwinds — this is why RAII works seamlessly with exceptions.
---
Exception Specifications
throw() (legacy, deprecated)
void f() throw(); // promises to throw nothing
void g() throw(int, char); // legacy spec: only throws int or char
Removed in C++17.
noexcept (modern, C++11+)
void f() noexcept; // promises no exception
void g() noexcept(true); // same
void h() noexcept(false); // may throw (default)
If a noexcept function throws, std::terminate() is called.
Move constructors and destructors should typically be noexcept for STL containers to use move optimisations.
---
Function-Try-Block — Constructor Exceptions
What if a constructor throws? The object is never constructed — its destructor will NOT run. Members already constructed are destroyed.
class A {
public:
A() { throw runtime_error("ctor failed"); }
~A() { cout << "~A\n"; }
};
class B {
A a;
public:
B() {
// a's ctor throws → B's destructor never runs
// BUT — a's destructor is NOT called (a never finished construction)
}
~B() { cout << "~B\n"; }
};
try {
B b; // a's ctor throws
}
catch (const exception& e) {
cout << "Caught: " << e.what();
}
// No destructor output — neither A nor B was fully constructed
To handle this:
B::B() try : a() {
// body
}
catch (...) { // function-try-block catch
// member 'a' or B::B body threw
throw; // rethrow — B object not constructed
}
---
Memory & Resources During Exceptions
Problem: new then throw before delete = leak.
void bad() {
int* p = new int[1000];
riskyOp(); // throws
delete[] p; // never reached — LEAK
}
Solutions:
- Smart pointers (C++11):
void good() {
auto p = make_unique<int[]>(1000); // auto-freed even on exception
riskyOp();
}
- Try/catch with explicit cleanup:
void okay() {
int* p = new int[1000];
try {
riskyOp();
delete[] p;
}
catch (...) {
delete[] p;
throw;
}
}
- RAII wrappers — encapsulate resources in classes with destructors.
---
Exception Handling vs Return Codes — comparison
| Aspect | Return Codes | Exceptions |
|---|---|---|
| Forgetting to check | Silent bug | Caught at runtime |
| Code clarity | Mixed with logic | Separated |
| Constructor failure | No mechanism | Throws |
| Propagation | Manual at each level | Automatic |
| Performance (happy path) | Slight cost (check) | Near-zero |
| Performance (error path) | Fast | Slow (stack unwind, RTTI) |
| Cross-DLL/API | Easy | Tricky (ABI issues) |
| Use | C, low-level, performance-critical | Most C++ code |
---
Common Pitfalls
1. Catching by value (slicing)
catch (exception e) { /* WRONG — slices derived parts */ }
catch (const exception& e) { /* RIGHT */ }
2. Order of catch blocks
catch (const exception& e) { /* catches everything */ }
catch (const runtime_error& e) { /* UNREACHABLE */ }
3. Exception in destructor
Throwing from a destructor during stack unwinding causes std::terminate. Destructors should never throw.
~Resource() noexcept { // mark noexcept to make intent clear
try { cleanup(); }
catch (...) { /* swallow */ }
}
---
Putting It All Together
#include <iostream>
#include <stdexcept>
#include <string>
using namespace std;
class BankAccount {
private:
string owner;
double balance;
public:
BankAccount(const string& o, double b) : owner(o), balance(b) {
if (b < 0) throw invalid_argument("Initial balance cannot be negative");
}
void deposit(double amount) {
if (amount <= 0) throw invalid_argument("Deposit must be positive");
balance += amount;
}
void withdraw(double amount) {
if (amount <= 0) throw invalid_argument("Withdrawal must be positive");
if (amount > balance) throw runtime_error("Insufficient funds");
balance -= amount;
}
double getBalance() const noexcept { return balance; }
};
int main() {
try {
BankAccount acc("Rohit", 1000);
acc.deposit(500);
acc.withdraw(2000); // throws — insufficient
}
catch (const invalid_argument& e) {
cout << "Input error: " << e.what() << endl;
}
catch (const runtime_error& e) {
cout << "Runtime error: " << e.what() << endl;
}
catch (const exception& e) {
cout << "Other error: " << e.what() << endl;
}
catch (...) {
cout << "Unknown error" << endl;
}
cout << "Program continues" << endl;
return 0;
}
---
Study deep
- Exceptions are for exceptional cases. Don't use them for normal control flow (like loop termination). Normal logic uses return values; exceptions signal errors that can't be handled locally.
- Exception safety levels (Stroustrup). Functions can offer four guarantees:
- No-throw: never throws (
noexcept) - Strong: state unchanged on exception (transactional)
- Basic: object valid but state may change
- No guarantee: undefined state after exception
- RAII makes exception safety automatic. Resources tied to object lifetime are released during stack unwinding. Modern C++ code that uses smart pointers and standard containers is largely exception-safe by default.
- Performance: zero cost on the happy path. Modern implementations (Itanium ABI) use exception tables, not runtime checks. If no exception is thrown, exception handling adds nothing. The cost is only paid on throw.
- Some codebases ban exceptions. Google C++ Style Guide forbids them in legacy code; the Linux kernel doesn't allow them. Game engines often disable for predictability. Modern application code embraces them.
Key Terms — Lesson 4.2
The terms below cover C++ exception handling — every PYQ on try/catch/throw expects fluent use.
Exception — A C++ object thrown to signal an error or abnormal condition that the surrounding code cannot handle locally. Exceptions propagate up the call stack until a matching catch handles them or until they reach main (where uncaught exceptions terminate the program).
try Block — A block of code in which exceptions may be thrown, monitored by one or more catch blocks. try { ... } defines the scope of exception monitoring; any exception thrown inside is checked against the attached handlers.
catch Block — A block that handles a specific type of exception: catch (const runtime_error& e) { ... }. Multiple catch blocks can follow a single try, each handling a different exception type. The first matching catch is executed; later ones are skipped.
throw Statement — The statement that raises an exception: throw runtime_error("Insufficient funds");. Stops normal execution and begins searching up the call stack for a matching handler.
Throwing vs Rethrowing — throw expr; raises a new exception. throw; (with no argument, inside a catch block) rethrows the current exception to be handled by an outer try. Common pattern: log the error then rethrow.
Generic catch (...) — A handler that catches any exception regardless of type. Useful as the last catch in a chain to log/cleanup before letting the program terminate. Cannot read the exception's details.
Exception Specification (throw(T)) — deprecated — A pre-C++11 mechanism to declare which exceptions a function could throw. Deprecated in C++11, removed in C++17. Replaced by noexcept.
noexcept (C++11) — A specifier that promises a function will not throw. void f() noexcept;. If a noexcept function does throw, std::terminate is called immediately — no stack unwinding, no handler invocation. Used heavily for move constructors / move assignment (the STL relies on them being noexcept).
Stack Unwinding — The process by which, after an exception is thrown, the C++ runtime destroys all automatic-storage (stack) objects from the throw point up to the catch handler, invoking each destructor in reverse construction order. RAII relies on this for resource cleanup.
Exception Hierarchy — The C++ standard library's exception class hierarchy, rooted at std::exception. Two main branches: std::logic_error (preventable programmer errors — invalid_argument, out_of_range, length_error, domain_error) and std::runtime_error (unforeseeable runtime conditions — range_error, overflow_error, underflow_error, system_error).
std::exception — The base class of all standard C++ exceptions. Provides a virtual what() method returning a C-string description. User-defined exceptions should typically inherit from std::exception (or a more specific subclass) to integrate with the standard catch chains.
std::logic_error — A branch of the exception hierarchy for programmer errors that could in principle be prevented — invalid arguments, out-of-range access, length violations. The bug-detection branch.
std::runtime_error — A branch for conditions detectable only at runtime — file not found, network failure, system error. The "world is wrong" branch.
std::bad_alloc — The specific exception thrown by new when allocation fails. Inherits from std::exception. Can be caught explicitly or via the base class.
std::bad_cast — The exception thrown by dynamic_cast to a reference type when the cast fails. (For pointer types, failed dynamic_cast returns nullptr without throwing.)
Exception Safety Guarantee (Stroustrup / Abrahams) — Levels of guarantee a function can offer about its behaviour under exceptions. No-throw: function never throws. Strong: if it throws, the program state is unchanged (transactional). Basic: all invariants preserved, no leaks, but state may have changed. No guarantee: state is undefined after exception.
RAII (Resource Acquisition Is Initialisation) — The C++ idiom that ties resource lifetime to object lifetime: constructor acquires, destructor releases. RAII makes exception safety automatic — if an exception is thrown, stack unwinding invokes the destructors that release the resources.
Resource Leak via Exception — A C-style failure mode where a function allocates a resource, an exception is thrown before deallocation, and the resource is permanently lost. RAII (smart pointers, container types) eliminates this risk — the destructor runs during stack unwinding regardless of how the function exits.
Exception vs Error Code — Two error-handling strategies. Exceptions separate normal logic from error handling, propagate automatically, integrate with RAII. Error codes are local, cheap, and predictable but require every caller to check and propagate manually. C++ supports both; modern best practice favours exceptions for exceptional cases and error codes / std::optional / std::expected (C++23) for expected failures.
Unwinding Cost / Cold Path — Modern C++ exception ABIs (Itanium) use exception tables instead of runtime checks. No cost is paid if no exception is thrown — the "happy path" is free. The full cost is only incurred when an exception actually propagates.
Function-try-block — A try block that wraps an entire function body, including the constructor initialiser list: Constructor() try : member(args) { ... } catch (...) { ... }. Useful for catching exceptions during member initialisation.
Catch by Reference — The recommended practice of catching exceptions by const reference (catch (const exception& e)). Catching by value can slice polymorphic exceptions; catching by pointer requires lifetime management.
User-Defined Exception — A class that inherits from a standard exception (or std::exception) and adds custom data. class InsufficientFunds : public runtime_error { ... };. Used to encode application-specific error categories.
Uncaught Exception — An exception that propagates out of main() without being caught, or escapes a noexcept function. The runtime calls std::terminate, which by default calls std::abort, terminating the program (often without running destructors of objects above main).
std::terminate / std::abort — The runtime functions invoked on uncaught exceptions or noexcept violations. std::terminate is customisable via std::set_terminate; the default implementation calls std::abort, which immediately ends the process.
---
PYQ pattern (very common): "What is exception handling? Explaintry,catch,throwwith example." — Define; show 3 keywords; example with division-by-zero or invalid input; explain propagation.
PYQ pattern: "What is generic catch? Explain rethrowing of exceptions." —catch(...)for any exception;throw;to rethrow current; example showing logging + rethrow.
PYQ pattern: "Explain types of exceptions in C++ standard library." —std::exceptionhierarchy; listlogic_error/runtime_errorfamilies;bad_alloc,bad_cast; show usage.