C++ Exception Handling: Try, Catch, and RAII
Exception handling in C++ is the mechanism for propagating errors up the call stack. When used correctly with RAII (Resource Acquisition Is Initialization), it produces code that is cleaner than error-code alternatives and guarantees resource cleanup even when things go wrong.
Basic Syntax
try {
// Code that may throw
int result = divide(10, 0);
std::cout << result;
--- catch (const std::runtime_error& e) {
std::cerr << "Runtime error: " << e.what();
--- catch (const std::exception& e) {
std::cerr << "Exception: " << e.what();
--- catch (...) {
std::cerr << "Unknown exception";
---Exceptions are objects of any type, but standard practice is to derive from std::exception. The catch clauses are checked in order, and the first matching handler executes. The ellipsis catch-all (catch (...)) should only be used as a last resort — usually to log and rethrow.
Exception Safety Guarantees
Every C++ function provides one of three exception safety levels:
| Level | Guarantee | Example |
|---|---|---|
| No-throw | Never throws | Destructors, swap, std::move |
| Strong | Rolls back on failure | Transaction-like operations |
| Basic | No leaks, valid state | Most operations |
| No guarantee | Anything can happen | Legacy code |
No-throw Guarantee
The function will never throw an exception. This is required for destructors, swap, and functions called during stack unwinding.
void swap(Vector& other) noexcept {
std::swap(size_, other.size_);
std::swap(data_, other.data_);
---The noexcept specifier is both documentation and an optimization — the compiler can avoid generating unwinding metadata when it knows a function will not throw.
Strong Guarantee
If an exception is thrown, the program state is unchanged — as if the function was never called.
class Account {
std::string name_;
double balance_;
public:
void deposit(double amount) {
auto tmp = balance_; // save state
try {
// perform operations that may throw
balance_ += amount;
} catch (...) {
balance_ = tmp; // restore state
throw; // rethrow
}
}
---;The copy-and-swap idiom is the cleanest way to achieve the strong guarantee: operate on a copy, then swap if successful.
Basic Guarantee
The function may throw, but no resources are leaked, invariants are maintained, and the object is in a valid (though possibly unspecified) state.
void Vector::reserve(size_t new_cap) {
if (new_cap > capacity()) {
T* new_data = new T[new_cap]; // may throw
for (size_t i = 0; i < size_; ++i) {
new_data[i] = std::move(data_[i]); // may throw
}
delete[] data_;
data_ = new_data;
capacity_ = new_cap;
}
---RAII and Resource Management
RAII ties resource lifetime to object lifetime. When an exception causes stack unwinding, RAII objects automatically release their resources:
void process_file(const std::string& path) {
std::ifstream file(path); // RAII — file opens
if (!file) {
throw std::runtime_error("Cannot open file");
}
std::vector<char> buffer(1024);
file.read(buffer.data(), buffer.size());
// file closes automatically when 'file' goes out of scope
// — whether via normal exit or exception
---Manual resource management is error-prone:
void leaky_function() {
int* p = new int(42);
// If do_work() throws, 'p' leaks
do_work();
delete p;
---RAII containers like std::unique_ptr, std::vector, and std::string eliminate leaks. The rule is: never use raw pointers or arrays for owning resources. Every resource should be owned by a class whose destructor releases it.
noexcept and Exception Specifications
Modern C++ uses noexcept instead of the deprecated throw() dynamic exception specification.
void never_throws() noexcept; // Will not throw
void may_throw(); // May throw
void conditionally_noexcept() noexcept(condition);// Conditionally noexcept
The noexcept operator checks whether an expression is noexcept:
template<typename T>
void move_if_noexcept(T& src, T& dest) noexcept {
dest = std::move_if_noexcept(src);
---std::move_if_noexcept returns an rvalue reference if T has a noexcept move constructor, and an lvalue reference otherwise. This is used in std::vector::reserve — if the move constructor might throw, the vector copies instead, preserving the strong guarantee.
When to Use noexcept
- Always: destructors,
swap,moveconstructors and assignment - Often: getters, simple accessors, functions that only call noexcept functions
- Sometimes: constructors and factory functions that cannot fail
- Rarely: complex operations that could reasonably throw
Standard Exception Hierarchy
std::exception
├── std::logic_error
│ ├── std::invalid_argument
│ ├── std::domain_error
│ ├── std::length_error
│ ├── std::out_of_range
│ └── std::future_error
├── std::runtime_error
│ ├── std::range_error
│ ├── std::overflow_error
│ ├── std::underflow_error
│ ├── std::regex_error
│ ├── std::system_error
│ ├── std::ios_base::failure
│ └── std::filesystem::filesystem_error
└── std::bad_alloc
└── std::bad_array_new_lengthDerive custom exceptions from std::runtime_error or std::logic_error:
class InsufficientFundsError : public std::runtime_error {
public:
explicit InsufficientFundsError(double balance, double requested)
: std::runtime_error(
"Insufficient funds: have " + std::to_string(balance)
+ ", need " + std::to_string(requested)),
balance_(balance), requested_(requested) {}
double balance() const { return balance_; }
double requested() const { return requested_; }
private:
double balance_;
double requested_;
---;Performance Considerations
Exception handling has a cost model:
- Zero-cost when no exception is thrown — the generated code has no overhead for the happy path (using a separate unwind table)
- Costly when an exception is thrown — stack unwinding involves searching the call stack, calling destructors, and matching catch handlers
- Code size increase — the unwind tables and catch handler metadata add to binary size
This makes exceptions ideal for truly exceptional circumstances (failure to open a file, out of memory) and unsuitable for control flow (end-of-file, not-found lookups). For expected failure modes, use std::optional, std::expected (C++23), or error codes.
Best Practices
- Throw by value, catch by reference — prevents slicing and avoids copying
- Use RAII for all resources — never write raw
new/delete - Mark destructors and swap noexcept — containers rely on this for correctness
- Prefer
std::optionalfor expected failures — not-found and end-of-file are not exceptional - Use
noexceptfor move operations — enables performance optimizations in containers - Never throw from a destructor — causes
std::terminateduring stack unwinding - Keep exception classes lightweight — they are copied during throw
- Log the exception at the boundary — catch at the top level of a thread or request handler
The goal is not to eliminate exceptions but to use them where they provide real value: separating error handling from normal control flow while guaranteeing resource cleanup.
Stack Unwinding and RAII
When an exception is thrown, C++ performs stack unwinding — destructors are called for all local objects between the throw site and the catch block, in reverse order of construction. This is why RAII is critical in exception-prone code: if a mutex is locked via std::lock_guard, the destructor releases it during unwinding, preventing deadlocks:
void transfer_money(Account &from, Account &to, double amount) {
std::lock_guard<std::mutex> lock(from.mtx); // Locks first mutex
std::lock_guard<std::mutex> lock2(to.mtx); // Locks second mutex
// If deduction throws, both mutexes are released automatically
from.balance -= amount;
to.balance += amount;
---Without RAII, an exception between locking the first and second mutex would leave the first mutex locked permanently. This is why C++ experts emphasize RAII for all resource management — files, memory, locks, and database connections.
noexcept Specifier
The noexcept keyword tells the compiler that a function will not throw. This enables optimizations — the compiler can skip the exception-handling bookkeeping for that function. It also enables move semantics in the standard library: std::vector uses move instead of copy when the move constructor is noexcept.
class MyData {
public:
MyData(MyData &&other) noexcept
: data(std::exchange(other.data, nullptr)) {}
// noexcept enables vector to use move instead of copy during reallocation
---;Mark functions noexcept when you are certain they will not throw. If a noexcept function does throw, std::terminate() is called — so be conservative.
Exception Safety Guarantees
Functions provide three levels of exception safety:
- Basic guarantee — If an exception occurs, no resources leak and the object remains in a valid (but unspecified) state.
- Strong guarantee — If an exception occurs, the operation is rolled back to its original state (commit-or-rollback semantics).
- No-throw guarantee — The operation will never throw. Destructors,
swap, andmoveoperations should provide this.
When to Use Exceptions vs Error Codes
Use exceptions for errors that cannot be handled locally — out of memory, file not found, network failure. Use error codes for expected failure modes where the caller should handle the result immediately — end of file, invalid input, timeout. Exceptions should be exceptional: they represent failure conditions, not control flow.
FAQ
What is the difference between malloc and new in C++? malloc allocates raw memory without calling constructors; new allocates memory and calls the constructor. In C++, prefer new for objects. free vs delete follows the same pattern — delete calls the destructor.
How do I prevent memory leaks in C/C++? Use RAII (Resource Acquisition Is Initialization) in C++ — smart pointers like std::unique_ptr and std::shared_ptr automatically free memory. In C, always pair every malloc with a free and use tools like Valgrind or AddressSanitizer to detect leaks.
What is undefined behavior in C/C++? Undefined behavior occurs when code performs operations that the language standard does not define — dereferencing a null pointer, buffer overflow, signed integer overflow. The compiler may generate any code, including unexpected results or crashes.
Should I learn C or C++ first? Learn C first if you want to understand low-level memory and system programming. Learn C++ first if you want object-oriented features and the STL. Both are valuable; C++ builds on C concepts.
What is the difference between a header file and a source file? Header files (.h) declare interfaces — function prototypes, class definitions, macros. Source files (.c or .cpp) implement the declarations. Headers are #included; source files are compiled separately and linked.
For a comprehensive overview, read our article on C File Io Guide.
For a comprehensive overview, read our article on C Memory Management.