Exception Handling: Error Propagation Mechanism
Exceptions provide a structured way to handle errors, separating error handling from normal flow. When an exception is thrown, stack unwinding occurs: all local objects are destroyed until a matching catch handler is found.
Basic Syntax
#include <stdexcept>
double divide(double a, double b) {
if (b == 0.0) {
throw std::runtime_error("Division by zero");
}
return a / b;
}
try {
double result = divide(10, 0);
} catch (const std::runtime_error& e) {
std::cerr << "Error: " << e.what() << '\\n';
} catch (const std::exception& e) {
std::cerr << "Exception: " << e.what() << '\\n';
} catch (...) {
std::cerr << "Unknown exception\\n";
}
Standard Exception Hierarchy
All standard exceptions derive from std::exception:
std::exception
├── std::logic_error
│ ├── std::invalid_argument
│ ├── std::domain_error
│ ├── std::length_error
│ └── std::out_of_range
├── std::runtime_error
│ ├── std::range_error
│ ├── std::overflow_error
│ └── std::underflow_error
└── std::bad_alloc
throw std::invalid_argument("Invalid input");
throw std::out_of_range("Index out of bounds");
throw std::bad_alloc(); // Memory allocation failed
Custom Exceptions
class FileError : public std::runtime_error {
std::string filename_;
public:
FileError(const std::string& file, const std::string& msg)
: std::runtime_error(msg), filename_(file) {}
const std::string& filename() const { return filename_; }
};
void openFile(const std::string& name) {
// ...
throw FileError(name, "Failed to open file");
}
try {
openFile("data.txt");
} catch (const FileError& e) {
std::cerr << "File error in " << e.filename()
<< ": " << e.what() << '\\n';
}
Exception Safety Guarantees
Code can provide different levels of exception safety:
1. No-Throw Guarantee
Operation never throws (or only throws if program terminates):
void swap(T& a, T& b) noexcept {
T temp = std::move(a);
a = std::move(b);
b = std::move(temp);
}
2. Strong Exception Safety (Transactional)
If an exception is thrown, program state is unchanged:
void push_back(const T& value) {
if (size_ == capacity_) {
// Allocate new buffer
T* new_data = new T[capacity_ * 2];
// Copy old data (might throw)
std::uninitialized_copy(data_, data_ + size_, new_data);
// Only now commit changes (no-throw operations)
delete[] data_;
data_ = new_data;
capacity_ *= 2;
}
data_[size_++] = value;
}
3. Basic Exception Safety
Invariants are preserved, no resources leak, but state may change:
void process() {
auto ptr = new Resource; // Potential leak if next line throws
risky_operation();
delete ptr;
}
// Better with RAII:
void process() {
auto ptr = std::make_unique<Resource>(); // Automatic cleanup
risky_operation();
}
4. No Exception Safety
Undefined behavior or resource leaks if exception thrown.
RAII and Exception Safety
RAII (Resource Acquisition Is Initialization) ensures cleanup even during exceptions:
class FileHandle {
FILE* file_;
public:
FileHandle(const char* name) : file_(fopen(name, "r")) {
if (!file_) throw std::runtime_error("Cannot open file");
}
~FileHandle() {
if (file_) fclose(file_); // Always called during stack unwinding
}
FileHandle(const FileHandle&) = delete;
FileHandle& operator=(const FileHandle&) = delete;
};
void process() {
FileHandle f("data.txt");
// Use file...
throw std::runtime_error("Error"); // File automatically closed
}
The noexcept Specifier
Declares that a function won’t throw exceptions:
void guaranteed() noexcept {
// If an exception escapes, std::terminate is called
}
template<typename T>
void swap(T& a, T& b) noexcept(noexcept(T(std::move(a)))) {
// noexcept if T's move constructor is noexcept
}
Importance: noexcept enables optimizations. Standard containers use move operations only if noexcept.
class Widget {
public:
Widget(Widget&&) noexcept; // vector will use move
// Widget(Widget&&); // vector will use copy instead (for safety)
};
Conditional noexcept
template<typename T>
class Stack {
public:
void push(const T& value) noexcept(std::is_nothrow_copy_constructible_v<T>) {
// noexcept if T is nothrow copyable
}
};
Rethrowing Exceptions
try {
risky_operation();
} catch (const std::exception& e) {
log_error(e.what());
throw; // Rethrow same exception (preserves type)
}
// DON'T do this:
catch (const std::exception& e) {
throw e; // Slices exception to std::exception!
}
Function try Blocks
Handle exceptions in constructor initialization:
class Widget {
Resource r_;
public:
Widget(const std::string& name)
try : r_(name) { // Function try block
// Constructor body
}
catch (const std::exception& e) {
// Handle exception from r_ construction
// Note: Exception is always rethrown after this catch block
}
};
Exception Specifications (Deprecated)
C++98 dynamic exception specifications are deprecated:
void func() throw(std::runtime_error); // Deprecated, don't use
void func() noexcept; // Modern C++
What happens when an exception is thrown but not caught?
Interactive Lab
Waiting for signal...