C++ Exceptions
Exceptions are C++’s built-in mechanism for handling errors that occur while a program is running, such as invalid input, failed memory allocation, or an out-of-range array access. Instead of checking a return code after every function call, you can throw an exception at the point where something goes wrong and catch it at a place higher up in the call stack that knows how to recover. This keeps error-handling code separate from normal logic and makes it much harder to accidentally ignore a failure.
Overview / How It Works
C++ exception handling revolves around three keywords: try, throw, and catch. You wrap risky code in a try block. If something inside that block calls throw with a value (almost always an object), the normal flow of execution stops immediately. The runtime then searches outward through the call stack for a matching catch block — one whose parameter type matches (or is a base class of) the thrown object’s type.
This search-and-unwind process is called stack unwinding. As the runtime walks back up the call stack looking for a handler, it destroys every local object in every function frame it passes through, calling each object’s destructor in reverse order of construction. This is precisely why RAII (Resource Acquisition Is Initialization) works so well with exceptions: if a resource is owned by a local object (a smart pointer, a file wrapper, a mutex lock), that object’s destructor still runs during unwinding, so the resource is released even though the function never reaches its normal return statement.
If no matching catch is found anywhere up the call stack, the special function std::terminate() is invoked, which by default calls abort() and ends the program. Because of this, an exception should always be caught somewhere — even if that somewhere is a generic handler in main.
C++ also provides a standard hierarchy of exception types rooted at std::exception (declared in <exception>), with more specific types in <stdexcept>. Every class in this hierarchy defines a virtual member function what() that returns a C-string describing the error. You can throw these standard types directly, or derive your own exception classes from std::exception to create domain-specific errors that still work with generic catch (const std::exception&) handlers.
| Type | Header | Typical use |
|---|---|---|
std::exception |
exception | Base class for all standard exceptions |
std::logic_error |
stdexcept | Errors detectable before runtime (bad arguments, bad logic) |
std::invalid_argument |
stdexcept | An argument’s value is not acceptable |
std::out_of_range |
stdexcept | An index or value is outside a valid range (e.g. vector::at) |
std::runtime_error |
stdexcept | Errors only detectable while the program runs |
std::overflow_error / underflow_error |
stdexcept | Arithmetic overflow/underflow |
std::bad_alloc |
new | Thrown by new when memory allocation fails |
Syntax
try {
// code that might throw
throw SomeExceptionType(arguments);
} catch (const ExceptionType1& e) {
// handle ExceptionType1
} catch (const ExceptionType2& e) {
// handle ExceptionType2
} catch (...) {
// catches literally anything not matched above
}
try— marks a block of code whose exceptions should be intercepted by the followingcatchclauses.throw expr;— raises an exception, copying/movingexprinto a temporary object that propagates up the stack.exprcan be of any type, but should almost always derive fromstd::exception.catch (Type e)— a handler that runs if the thrown object’s type matchesType(or is convertible to it). Handlers are tried top-to-bottom, and only one runs.- catch by
const Type&— the idiomatic form; avoids copying and, crucially, avoids slicing derived exception objects (explained below). catch (...)— a catch-all handler with no access to the thrown value’s type; useful as a last resort, typically to log and rethrow or terminate gracefully.
Examples
Example 1: Basic try/catch/throw
#include <iostream>
#include <stdexcept>
double divide(double a, double b) {
if (b == 0.0) {
throw std::runtime_error("Division by zero");
}
return a / b;
}
int main() {
try {
std::cout << divide(10, 2) << "\n";
std::cout << divide(5, 0) << "\n";
std::cout << "This line never runs\n";
} catch (const std::runtime_error& e) {
std::cout << "Caught exception: " << e.what() << "\n";
}
std::cout << "Program continues after catch\n";
return 0;
}
Output:
5
Caught exception: Division by zero
Program continues after catch
The first call to divide succeeds and prints normally. The second call throws before returning a value, so the line that would print its result and the line after it inside the try block never execute. Control jumps straight to the matching catch, and execution resumes normally afterward — the program does not crash.
Example 2: Multiple catch blocks
#include <iostream>
#include <vector>
#include <stdexcept>
void processIndex(const std::vector<int>& v, int index) {
if (index < 0) {
throw std::invalid_argument("Index cannot be negative");
}
std::cout << "Value: " << v.at(index) << "\n";
}
int main() {
std::vector<int> numbers = {10, 20, 30};
int testIndices[] = {1, -5, 10};
for (int idx : testIndices) {
try {
processIndex(numbers, idx);
} catch (const std::invalid_argument& e) {
std::cout << "Invalid argument: " << e.what() << "\n";
} catch (const std::out_of_range& e) {
std::cout << "Out of range access attempted\n";
} catch (const std::exception& e) {
std::cout << "Unknown standard exception: " << e.what() << "\n";
}
}
return 0;
}
Output:
Value: 20
Invalid argument: Index cannot be negative
Out of range access attempted
Each iteration is wrapped in its own try, so one bad index doesn’t stop the loop. The negative index is rejected manually before at() is even called, throwing std::invalid_argument. The index 10 is out of bounds for a 3-element vector, so vector::at itself throws std::out_of_range. Notice the order of the catch clauses matters: the compiler tries them top to bottom, and since out_of_range and invalid_argument both derive from std::logic_error (not from each other), listing the specific types before the generic std::exception& catch-all ensures the precise message is used.
Example 3: Custom exception classes
#include <iostream>
#include <exception>
#include <string>
class InsufficientFundsException : public std::exception {
private:
std::string message;
public:
InsufficientFundsException(double balance, double amount) {
message = "Cannot withdraw " + std::to_string(amount) +
": balance is only " + std::to_string(balance);
}
const char* what() const noexcept override {
return message.c_str();
}
};
class BankAccount {
private:
double balance;
public:
BankAccount(double initialBalance) : balance(initialBalance) {}
void withdraw(double amount) {
if (amount > balance) {
throw InsufficientFundsException(balance, amount);
}
balance -= amount;
std::cout << "Withdrew " << amount << ", new balance: " << balance << "\n";
}
};
int main() {
BankAccount account(100.0);
try {
account.withdraw(30.0);
account.withdraw(200.0);
} catch (const InsufficientFundsException& e) {
std::cout << "Error: " << e.what() << "\n";
}
return 0;
}
Output:
Withdrew 30, new balance: 70
Error: Cannot withdraw 200.000000: balance is only 70.000000
Deriving from std::exception and overriding what() lets your custom error type plug into any code that already knows how to handle standard exceptions, while still carrying whatever extra data you need. The what() override is marked const noexcept to match the base class signature exactly — if you forget noexcept here, most compilers will still compile it (a stricter exception specification is allowed), but matching it exactly is best practice.
Under the Hood: Stack Unwinding
#include <iostream>
#include <stdexcept>
#include <string>
class Resource {
private:
std::string name;
public:
Resource(const std::string& n) : name(n) {
std::cout << "Acquiring " << name << "\n";
}
~Resource() {
std::cout << "Releasing " << name << "\n";
}
};
void inner() {
Resource r2("inner resource");
throw std::runtime_error("Something went wrong in inner()");
}
void outer() {
Resource r1("outer resource");
inner();
std::cout << "This never prints\n";
}
int main() {
try {
outer();
} catch (const std::exception& e) {
std::cout << "Caught: " << e.what() << "\n";
}
return 0;
}
Output:
Acquiring outer resource
Acquiring inner resource
Releasing inner resource
Releasing outer resource
Caught: Something went wrong in inner()
Step by step: main calls outer, which constructs r1 and calls inner. inner constructs r2 and then throws. At that instant the runtime stops running inner‘s remaining code and begins unwinding: it destroys r2 (printing "Releasing inner resource"), pops inner‘s frame, then destroys r1 (printing "Releasing outer resource") and pops outer‘s frame. Only after every local object between the throw point and the handler has been destroyed does control reach the catch block in main. This guarantee — that destructors always run during unwinding — is what makes RAII wrappers like std::unique_ptr, std::lock_guard, and file streams exception-safe by default.
Common Mistakes
Mistake 1: Catching by value slices derived exceptions
class MyException : public std::exception {
public:
const char* what() const noexcept override { return "custom failure"; }
};
try {
throw MyException();
} catch (std::exception e) { // caught BY VALUE
std::cout << e.what() << std::endl; // may lose the derived class's behavior
}
Catching by value copy-constructs a plain std::exception from the thrown MyException, a phenomenon called object slicing: the derived-class portion is discarded, and any virtual override behavior beyond what the base class already provides can be lost or, worse, cause subtly wrong output in more complex hierarchies. The fix is simple — always catch exceptions by const reference:
#include <iostream>
#include <exception>
class MyException : public std::exception {
public:
const char* what() const noexcept override { return "custom failure"; }
};
int main() {
try {
throw MyException();
} catch (const std::exception& e) { // caught by reference: no slicing
std::cout << e.what() << std::endl;
}
return 0;
}
Output:
custom failure
Catching by reference binds directly to the original polymorphic object, so virtual dispatch through what() (or any other virtual function) works exactly as intended, and no unnecessary copy is made.
Mistake 2: Letting an exception escape a destructor
class RiskyResource {
public:
~RiskyResource() {
throw std::runtime_error("cleanup failed"); // DANGEROUS
}
};
If a destructor throws while the stack is already unwinding due to another exception, the C++ runtime cannot decide which exception should propagate, so it calls std::terminate() immediately and the program aborts — even if you have a perfectly good catch block waiting. Destructors are implicitly noexcept by default in modern C++ for exactly this reason. The fix is to catch and handle any error inside the destructor itself, never letting it escape:
#include <iostream>
#include <stdexcept>
class SafeResource {
public:
~SafeResource() {
try {
riskyCleanup();
} catch (const std::exception& e) {
std::cout << "Cleanup error handled internally: " << e.what() << std::endl;
}
}
private:
void riskyCleanup() {
throw std::runtime_error("cleanup failed");
}
};
int main() {
{
SafeResource res;
std::cout << "Using resource\n";
}
std::cout << "Program continues safely\n";
return 0;
}
Output:
Using resource
Cleanup error handled internally: cleanup failed
Program continues safely
By wrapping the risky call in its own try/catch inside the destructor, the error is fully absorbed before the destructor returns, and the rest of the program continues normally instead of terminating.
Best Practices
- Always catch by
const reference(e.g.catch (const std::exception& e)) to avoid slicing and unnecessary copies. - Throw objects, not raw pointers — throwing
new SomeError()forces every catcher to remember todeleteit, which is easy to forget and leaks memory. - Derive custom exceptions from
std::exception(or a more specific standard type likestd::runtime_error) so generic handlers can still catch them. - Never let an exception escape a destructor; catch and handle errors internally, since destructors run during unwinding and a second exception there triggers
std::terminate(). - Use exceptions for truly exceptional, rare failure conditions — not for routine control flow like "end of loop" or expected validation failures, which are better handled with normal return values.
- Order
catchclauses from most specific to least specific; a base-class handler placed first will silently swallow all derived types below it. - Mark functions that are guaranteed not to throw with
noexcept— this documents intent and can enable compiler optimizations. - Prefer RAII wrappers (
std::unique_ptr,std::lock_guard, standard containers) over manual cleanup so stack unwinding automatically releases resources.
Practice Exercises
Exercise 1: Write a function int safeDivide(int a, int b) that throws std::invalid_argument if b is zero, otherwise returns a / b. Call it from main inside a try/catch block with both a valid and an invalid input, printing either the result or the caught error message.
Exercise 2: Create a custom exception class NegativeAgeException derived from std::exception that stores the invalid age and returns a descriptive message from what(). Write a Person class whose constructor throws this exception if the given age is negative, and test it with both a valid and an invalid age.
Exercise 3: Write a program with a Logger class whose destructor prints "Logger closed". In main, create a Logger object inside a try block, then throw a std::runtime_error after creating it, and catch it in main. Verify (by the printed order of output) that the destructor runs during stack unwinding, before the catch block’s message is printed.
Summary
try/throw/catchlet you separate error detection from error handling, transferring control from the point of failure to a handler further up the call stack.- When an exception is thrown, the stack unwinds: local objects between the throw site and the matching handler are destroyed in reverse order, which is why RAII and exceptions work well together.
- If no matching handler exists anywhere on the stack,
std::terminate()is called and the program aborts. - The standard library provides a hierarchy rooted at
std::exception, withwhat()returning a descriptive message; deriving your own exceptions from it keeps them compatible with generic handlers. - Always catch by
const referenceto avoid slicing, and never let exceptions escape a destructor. - Use exceptions for genuinely exceptional error conditions, not as a substitute for normal control flow.
