Exception Handling in C++
Exception handling is a mechanism for dealing with errors or unexpected conditions while a program is running. Without exception handling, a program crashes immediately when an error occurs. With exception handling, we can catch errors and handle them gracefully without stopping the entire program.
C++ provides four main keywords for exception handling:
try— A block of code that might raise an exception.catch— A block that catches and handles an exception.throw— Throws an exception when an error occurs.catch(...)— Catches all kinds of exceptions (catch-all).
In addition, there is the noexcept concept (C++11), which indicates that a function will not throw an exception, and stack unwinding, which happens when an exception is thrown and caught — all local objects on the stack frames are automatically destroyed in reverse order.
Exception handling allows separation between error-handling code and normal code (clean code), so the program is easier to read and maintain.
Code Example
#include <iostream>
#include <string>
#include <fstream>
#include <stdexcept>
#include <vector>
#include <cmath>
using namespace std;
// ==================== Custom Exception Classes ====================
class DivisionByZeroException : public exception {
private:
string message;
public:
DivisionByZeroException(string op) {
message = "Error: Division by zero in operation " + op;
}
const char* what() const noexcept override {
return message.c_str();
}
};
class InvalidAgeException : public exception {
private:
int age;
public:
InvalidAgeException(int a) : age(a) {}
const char* what() const noexcept override {
static string msg;
msg = "Error: Invalid age: " + to_string(age);
return msg.c_str();
}
int getAge() const { return age; }
};
class FileNotFoundException : public runtime_error {
public:
FileNotFoundException(const string& filename)
: runtime_error("File not found: " + filename) {}
};
// ==================== Functions with Exceptions ====================
double divide(double a, double b) {
if (b == 0) {
throw DivisionByZeroException("division");
}
return a / b;
}
void validateAge(int age) {
if (age < 0 || age > 150) {
throw InvalidAgeException(age);
}
cout << "Age is valid: " << age << endl;
}
void readFile(const string& filename) {
ifstream file(filename);
if (!file.is_open()) {
throw FileNotFoundException(filename);
}
string line;
while (getline(file, line)) {
cout << line << endl;
}
file.close();
}
int riskyOperation(int a, int b) {
cout << "Starting operation..." << endl;
int result = divide(a, b);
cout << "Operation finished." << endl;
return result;
}
// ==================== Functions with noexcept ====================
void criticalFunction() noexcept {
// This function is guaranteed not to throw an exception
cout << "Critical function running." << endl;
}
int main() {
// ==================== Basic try-catch ====================
cout << "=== Basic try-catch ===" << endl;
try {
int result = divide(10, 0);
cout << "Result: " << result << endl;
} catch (const DivisionByZeroException& e) {
cout << e.what() << endl;
}
try {
int result = divide(10, 3);
cout << "Result: " << result << endl;
} catch (const DivisionByZeroException& e) {
cout << e.what() << endl;
}
// ==================== Multiple Catch Blocks ====================
cout << "\n=== Multiple Catch Blocks ===" << endl;
try {
validateAge(-5);
} catch (const InvalidAgeException& e) {
cout << e.what() << endl;
cout << "Age entered: " << e.getAge() << endl;
}
try {
validateAge(25);
} catch (const InvalidAgeException& e) {
cout << e.what() << endl;
}
// ==================== Catch-All ====================
cout << "\n=== Catch-All (...) ===" << endl;
try {
throw 42; // Throwing an int (not an exception class)
} catch (int e) {
cout << "Caught int: " << e << endl;
}
try {
throw string("Error string!");
} catch (...) {
cout << "Caught an unknown exception." << endl;
}
// ==================== Exception Hierarchy ====================
cout << "\n=== Exception Hierarchy ===" << endl;
try {
throw invalid_argument("Invalid argument!");
} catch (const invalid_argument& e) {
cout << "invalid_argument: " << e.what() << endl;
} catch (const runtime_error& e) {
cout << "runtime_error: " << e.what() << endl;
} catch (const exception& e) {
cout << "exception: " << e.what() << endl;
} catch (...) {
cout << "Unknown exception." << endl;
}
// ==================== Stack Unwinding ====================
cout << "\n=== Stack Unwinding ===" << endl;
try {
int result = riskyOperation(10, 0);
cout << "Result: " << result << endl;
} catch (const DivisionByZeroException& e) {
cout << "Caught in main: " << e.what() << endl;
}
// ==================== Re-throwing an Exception ====================
cout << "\n=== Re-throwing ===" << endl;
try {
try {
throw runtime_error("Original error");
} catch (const runtime_error& e) {
cout << "Inner catch: " << e.what() << endl;
cout << "Re-throwing..." << endl;
throw; // Re-throw the same exception
}
} catch (const runtime_error& e) {
cout << "Outer catch: " << e.what() << endl;
}
// ==================== File Not Found ====================
cout << "\n=== File Not Found ===" << endl;
try {
readFile("nonexistent.txt");
} catch (const FileNotFoundException& e) {
cout << e.what() << endl;
}
// ==================== noexcept ====================
cout << "\n=== noexcept ===" << endl;
try {
criticalFunction();
cout << "Critical function finished without exception." << endl;
} catch (...) {
cout << "Will not reach here." << endl;
}
// ==================== Best Practices ====================
cout << "\n=== Best Practices ===" << endl;
cout << "1. Use an exception class derived from std::runtime_error or std::exception as the base." << endl;
cout << "2. Catch exceptions by reference (const&) to avoid slicing." << endl;
cout << "3. Order catch blocks from the most specific to the most general." << endl;
cout << "4. Use noexcept for functions guaranteed not to throw exceptions." << endl;
cout << "5. Use RAII for resource management so stack unwinding is safe." << endl;
return 0;
}
Code Explanation
-
class DivisionByZeroException : public exception— A custom exception inheriting fromstd::exception. Overrides thewhat()method to provide an informative error message. -
throw DivisionByZeroException("division")— Throws a custom exception with a message describing the type of operation that failed. -
catch (const DivisionByZeroException& e)— Catches an exception of a specific type. Usesconst&to avoid slicing and to avoid copying the exception object. -
catch(...)— A catch-all block. Catches all kinds of exceptions not caught by previous catch blocks. It must be placed last. -
FileNotFoundException : public runtime_error— A custom exception inheriting fromstd::runtime_error, which is part of the C++ exception hierarchy. -
noexcept— A specification that a function will not throw an exception. Useful for optimization and for critical functions. -
throw;— Re-throwing an exception. Throws back the currently caught exception without modifying it. -
Stack unwinding — When an exception is thrown, all local objects on the stack frames are automatically destroyed in reverse order. This ensures resources are freed properly.
-
Exception hierarchy — C++ has an exception hierarchy:
exception→runtime_error/logic_error→ their derivatives. Catch from the most specific to the most general.
Everyday Use Cases
Imagine you are building a registration form validation system. Exception handling helps deal with various kinds of errors in a structured way:
// Registration form validation system
class ValidationError : public exception {
string field;
string message;
public:
ValidationError(string f, string m) : field(f), message(m) {}
string getField() const { return field; }
const char* what() const noexcept override {
return message.c_str();
}
};
void validateForm(const string& name, const string& email, int age) {
if (name.empty()) throw ValidationError("name", "Name must not be empty");
if (email.find('@') == string::npos)
throw ValidationError("email", "Email is not valid");
if (age < 17) throw ValidationError("age", "Minimum age is 17 years");
cout << "Form valid! Processing registration..." << endl;
}
// Using it
try {
validateForm("", "invalid-email", 15);
} catch (const ValidationError& e) {
cout << "Field '" << e.getField() << "': " << e.what() << endl;
// Display the error message next to the problematic field
}
Conclusion
Exception handling enables structured and graceful error handling. Use try-catch to catch and handle exceptions, throw to throw an exception, and catch(...) as the last-resort catcher. Create custom exceptions by inheriting from std::exception or std::runtime_error. Always catch exceptions by reference (const&) and order them from specific to general. Use RAII and noexcept for safe resource management.