Skillversitas

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

  1. class DivisionByZeroException : public exception — A custom exception inheriting from std::exception. Overrides the what() method to provide an informative error message.

  2. throw DivisionByZeroException("division") — Throws a custom exception with a message describing the type of operation that failed.

  3. catch (const DivisionByZeroException& e) — Catches an exception of a specific type. Uses const& to avoid slicing and to avoid copying the exception object.

  4. catch(...) — A catch-all block. Catches all kinds of exceptions not caught by previous catch blocks. It must be placed last.

  5. FileNotFoundException : public runtime_error — A custom exception inheriting from std::runtime_error, which is part of the C++ exception hierarchy.

  6. noexcept — A specification that a function will not throw an exception. Useful for optimization and for critical functions.

  7. throw; — Re-throwing an exception. Throws back the currently caught exception without modifying it.

  8. 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.

  9. 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.