Exception Handling in C++
Every program encounters unexpected situations: a file that does not exist, a division by zero, a network timeout, or user input that violates an assumption. How a program behaves in these moments defines its quality. C++ provides a structured mechanism for handling such situations — exceptions — which separate the normal flow of code from the error-handling flow, making both cleaner.
In FAANG interview circuits, exception safety is a recurring topic. Understanding it also matters in production code at companies like Infosys, Wipro, or Razorpay, where a crash in a financial transaction handler can have real consequences.
The Basics: try, catch, throw
#include <iostream>
#include <stdexcept>
int safeDivide(int a, int b) {
if (b == 0) {
throw std::invalid_argument("Division by zero is not allowed");
}
return a / b;
}
int main() {
try {
int result = safeDivide(10, 0);
std::cout << "Result: " << result << "\n";
}
catch (const std::invalid_argument& e) {
std::cerr << "Caught exception: " << e.what() << "\n";
}
std::cout << "Program continues normally after exception.\n";
return 0;
}
Output:
Caught exception: Division by zero is not allowed
Program continues normally after exception.
throwraises an exception. Execution immediately leaves the current function and travels up the call stack searching for a matchingcatchblock.trymarks a block of code to monitor for exceptions.catchdefines a handler for a specific exception type.
The Standard Exception Hierarchy
C++ provides a family of exception classes in <stdexcept> and <exception>. Understanding this hierarchy lets you write targeted handlers.
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
| Class | When to use |
|---|---|
std::logic_error | Bugs that should have been caught at compile time or by assertions |
std::invalid_argument | A function argument violates a precondition |
std::out_of_range | Index or value is outside an acceptable range |
std::runtime_error | Errors detectable only at runtime (network, file I/O) |
std::overflow_error | Arithmetic overflow |
std::range_error | Result of a computation is outside a representable range |
All standard exception classes expose a .what() method that returns a const char* description.
Catching by Reference
Always catch exceptions by reference to const — not by value. Catching by value slices the object, losing information from derived exception classes.
try {
throw std::runtime_error("Disk full");
}
catch (const std::exception& e) { // catches any std::exception
std::cerr << e.what() << "\n";
}
You can chain multiple catch blocks. Place more specific types before more general ones:
try {
// ... code that may throw
}
catch (const std::invalid_argument& e) {
std::cerr << "Invalid argument: " << e.what() << "\n";
}
catch (const std::runtime_error& e) {
std::cerr << "Runtime error: " << e.what() << "\n";
}
catch (const std::exception& e) {
std::cerr << "Unknown std exception: " << e.what() << "\n";
}
catch (...) {
std::cerr << "Unknown non-std exception caught\n";
}
The catch (...) block is a catch-all that handles anything, including integers or strings thrown by mistake. Use it as the last resort.
Rethrowing Exceptions
Sometimes you want to log or partially handle an exception and then let it propagate further. Use throw; (with no argument) inside a catch block to rethrow the current exception without slicing it.
void processPayment(double amount) {
try {
if (amount <= 0) {
throw std::invalid_argument("Payment amount must be positive");
}
// ... actual payment processing
}
catch (const std::exception& e) {
std::cerr << "[processPayment] Logging error: " << e.what() << "\n";
throw; // rethrow to the caller
}
}
int main() {
try {
processPayment(-500.0);
}
catch (const std::invalid_argument& e) {
std::cerr << "[main] Handled: " << e.what() << "\n";
}
}
The noexcept Specifier (C++11)
The noexcept specifier declares that a function will not throw exceptions. This is a contract: if the function does throw, std::terminate is called immediately.
int add(int a, int b) noexcept {
return a + b;
}
Marking functions noexcept has two benefits:
- The compiler can apply optimisations (especially for move constructors and destructors).
- It communicates intent clearly to callers.
When to use noexcept
| Case | Use noexcept? |
|---|---|
| Destructors | Always (destructors are implicitly noexcept) |
| Move constructors and move assignment | Yes — critical for STL container efficiency |
| Simple arithmetic/utility functions | Yes |
| Functions that call code which can throw | No |
class Buffer {
public:
Buffer(Buffer&& other) noexcept // move constructor — safe to mark noexcept
: data_(other.data_), size_(other.size_) {
other.data_ = nullptr;
other.size_ = 0;
}
~Buffer() noexcept { delete[] data_; }
private:
char* data_;
std::size_t size_;
};
Custom Exception Classes
Defining your own exception classes allows callers to distinguish your errors from generic library errors and to carry domain-specific information.
#include <stdexcept>
#include <string>
class DatabaseError : public std::runtime_error {
public:
explicit DatabaseError(const std::string& msg, int errorCode)
: std::runtime_error(msg), errorCode_(errorCode) {}
int errorCode() const noexcept { return errorCode_; }
private:
int errorCode_;
};
class ConnectionError : public DatabaseError {
public:
explicit ConnectionError(const std::string& host)
: DatabaseError("Cannot connect to database host: " + host, 5001),
host_(host) {}
const std::string& host() const noexcept { return host_; }
private:
std::string host_;
};
Usage:
void connectDB(const std::string& host) {
if (host.empty()) {
throw ConnectionError("(empty)");
}
// attempt connection ...
throw ConnectionError(host); // simulate failure
}
int main() {
try {
connectDB("db.internal.mycompany.in");
}
catch (const ConnectionError& e) {
std::cerr << "Connection failed to " << e.host()
<< " (code " << e.errorCode() << "): " << e.what() << "\n";
}
catch (const DatabaseError& e) {
std::cerr << "Database error (code " << e.errorCode() << "): " << e.what() << "\n";
}
}
RAII and Exceptions
RAII (Resource Acquisition Is Initialisation) is a C++ idiom where resources (memory, file handles, locks) are tied to the lifetime of objects. When an exception unwinds the stack, destructors run automatically, releasing resources without any extra cleanup code.
#include <fstream>
#include <stdexcept>
class ScopedFile {
public:
explicit ScopedFile(const std::string& path) : file_(path) {
if (!file_.is_open()) {
throw std::runtime_error("Cannot open file: " + path);
}
}
~ScopedFile() {
// destructor always runs — even during stack unwinding
if (file_.is_open()) file_.close();
}
std::ifstream& get() { return file_; }
private:
std::ifstream file_;
};
void readConfig(const std::string& path) {
ScopedFile cfg(path); // throws if file missing
std::string line;
while (std::getline(cfg.get(), line)) {
// process line
}
} // ScopedFile destructor closes the file here, even if an exception occurs above
This is why modern C++ prefers smart pointers, std::fstream, and std::lock_guard over raw resource management — they implement RAII for you.
Worked Example: Safe Division and File Reading
#include <fstream>
#include <iostream>
#include <sstream>
#include <stdexcept>
#include <string>
#include <vector>
// Custom exception for our application
class AppError : public std::runtime_error {
public:
explicit AppError(const std::string& msg) : std::runtime_error(msg) {}
};
// Safe integer division — throws on zero divisor
double safeDivide(double numerator, double denominator) {
if (denominator == 0.0) {
throw std::invalid_argument("Denominator cannot be zero");
}
return numerator / denominator;
}
// Read a file and return its lines — throws if file cannot be opened
std::vector<std::string> readFileLines(const std::string& path) {
std::ifstream file(path);
if (!file.is_open()) {
throw AppError("File not found or cannot be read: " + path);
}
std::vector<std::string> lines;
std::string line;
while (std::getline(file, line)) {
if (!line.empty()) {
lines.push_back(line);
}
}
if (lines.empty()) {
throw AppError("File is empty: " + path);
}
return lines;
}
// Compute class average from a marks file
// File format: one integer mark per line
double computeClassAverage(const std::string& filePath) {
auto lines = readFileLines(filePath); // may throw AppError
double sum = 0.0;
int count = 0;
for (const auto& line : lines) {
try {
double mark = std::stod(line); // may throw std::invalid_argument
sum += mark;
++count;
}
catch (const std::invalid_argument&) {
std::cerr << "Warning: Skipping non-numeric line: '" << line << "'\n";
}
}
return safeDivide(sum, count); // may throw std::invalid_argument if count is 0
}
int main() {
// Test safe division
std::cout << "--- Safe Division ---\n";
try {
std::cout << "10 / 4 = " << safeDivide(10, 4) << "\n";
std::cout << "10 / 0 = " << safeDivide(10, 0) << "\n"; // throws
}
catch (const std::invalid_argument& e) {
std::cerr << "Division error: " << e.what() << "\n";
}
// Test file reading with a valid file
std::cout << "\n--- File Reading ---\n";
try {
double avg = computeClassAverage("marks.txt");
std::cout << "Class average: " << avg << "\n";
}
catch (const AppError& e) {
std::cerr << "Application error: " << e.what() << "\n";
}
catch (const std::invalid_argument& e) {
std::cerr << "Data error: " << e.what() << "\n";
}
catch (const std::exception& e) {
std::cerr << "Unexpected error: " << e.what() << "\n";
}
// Test file reading with a missing file
std::cout << "\n--- Missing File ---\n";
try {
computeClassAverage("nonexistent_marks.txt");
}
catch (const AppError& e) {
std::cerr << "Caught AppError: " << e.what() << "\n";
}
std::cout << "\nProgram finished cleanly.\n";
return 0;
}
Sample output (assuming marks.txt does not exist):
--- Safe Division ---
10 / 4 = 2.5
Division error: Denominator cannot be zero
--- File Reading ---
Application error: File not found or cannot be read: marks.txt
--- Missing File ---
Caught AppError: File not found or cannot be read: nonexistent_marks.txt
Program finished cleanly.
Common Pitfalls
1. Catching by value instead of by const reference
Catching std::exception e (by value) copies the object and slices off the derived type. The dynamic type is lost and .what() may return the wrong message.
// WRONG
catch (std::exception e) { ... }
// CORRECT
catch (const std::exception& e) { ... }
2. Catching a more general type before a specific one
catch blocks are checked in order. Placing catch (const std::exception&) before catch (const std::runtime_error&) means the runtime_error block is unreachable.
3. Throwing from a destructor
If a destructor throws during stack unwinding from another exception, std::terminate is called immediately. Destructors must be noexcept (they are by default in C++11 onward). Swallow exceptions inside destructors.
4. Using exceptions for normal control flow
Exceptions have overhead (stack unwinding, RTTI). Do not throw exceptions for expected conditions like "value not found in a collection." Use return codes, std::optional, or std::expected for those.
5. Throwing and catching integers or raw strings
throw 42; or throw "error"; is legal but makes handlers fragile and loses the .what() interface. Always throw objects derived from std::exception.
Practice Exercises
-
Write a function
parseInt(const std::string& s)that throwsstd::invalid_argumentif the string is not a valid integer, andstd::out_of_rangeif it exceedsINT_MAX. Test it with"abc","99999999999999", and"42". -
Create a custom exception class
NetworkErrorthat inherits fromstd::runtime_errorand stores an HTTP status code. Write a functionfetchData(int statusCode)that throwsNetworkErrorfor status codes 400 and above. -
Write a stack class (
IntStack) withpushandpopmethods.popshould throwstd::underflow_errorwhen the stack is empty. Write a test program that demonstrates catching this error. -
Implement a
SafeArrayclass wrapping astd::vector<int>. Override the subscript operator to throwstd::out_of_rangewith a helpful message when the index is invalid. -
Write a function
loadConfig(const std::string& path)that reads a config file and throws a customConfigErrorif the file is missing, and a differentParseErrorif a line is malformed. Demonstrate both scenarios. -
Explore
std::terminate: write a destructor that throws, and observe the program crash. Then fix it by catching and suppressing the exception inside the destructor.
Summary
throwraises an exception;trymonitors a block;catchhandles specific exception types.- Always catch exceptions by
constreference to avoid object slicing. - The standard exception hierarchy is rooted at
std::exception; all standard exceptions expose.what(). - Place more specific
catchblocks before more general ones;catch (...)is the catch-all of last resort. - Use bare
throw;inside acatchblock to rethrow the current exception without slicing it. noexceptdeclares that a function will not throw; violating this contract callsstd::terminate.- Custom exception classes should inherit from
std::runtime_errororstd::logic_errorand add domain-specific fields. - RAII ensures that destructors release resources automatically during stack unwinding — no cleanup code needed in
catchblocks. - Never throw from destructors; always mark destructors
noexcept. - Use exceptions for truly exceptional situations, not for routine flow control.