C, C++, MFC  

Chapter 17: Exception Handling: Robust Error Management in C++

Previous chapter: Chapter 16: STL: Iterators and Algorithms in C++

In real-world applications, errors are inevitable. Exception handling is the C++ mechanism that cleanly separates the normal execution logic from the error handling logic, ensuring that programs can gracefully respond to unexpected runtime problems.

1. The Need for Exception Handling

Traditional C-style error handling often relies on checking return values (e.g., a function returns $-1$ on failure). This can lead to messy, hard-to-read code. Exceptions allow you to throw an error object from the point of failure and catch it at a high-level function responsible for recovery.

2. The try, catch, and throw Mechanism

Exception handling involves three keywords:

  1. try: A block of code where you anticipate an exception might occur is enclosed in a try block.

  2. throw: When an error condition is met within the try block, the program creates and throws an exception object (often a string or a custom class).

  3. catch: A block of code immediately following the try block that is designed to handle a specific type of exception.

void divide(int numerator, int denominator) {
    if (denominator == 0) {
        throw std::runtime_error("Error: Cannot divide by zero.");
    }
    std::cout << numerator / denominator << std::endl;
}

int main() {
    try {
        divide(10, 0); // This will throw an exception
    } catch (const std::runtime_error& e) {
        // This catches the thrown exception
        std::cerr << "Caught Exception: " << e.what() << std::endl;
    }
    std::cout << "Program continues execution." << std::endl;
    return 0;
}

3. Multiple catch Blocks

A try block can be followed by multiple catch blocks, each handling a different type of exception. The exception is caught by the first matching type.

try {
    // ... code that might throw different exceptions
} catch (const std::out_of_range& e) {
    // Handle specific array boundary errors
} catch (const std::exception& e) {
    // Handle all other standard exceptions (polymorphism)
} catch (...) {
    // Catch-all block for unknown exception types
}

4. Custom Exception Classes

For sophisticated error reporting, it is best practice to define your own exception classes, usually by inheriting from std::exception (or one of its derived classes like std::runtime_error).

#include <exception>

class FileIOException : public std::exception {
public:
    const char* what() const noexcept override {
        return "Custom Error: File could not be opened for reading.";
    }
};

// ... inside a function
throw FileIOException();

5. RAII: Resource Acquisition Is Initialization

Exception handling makes the RAII pattern essential. RAII states that resource acquisition (like opening a file or allocating memory) should occur during object initialization (in the constructor). The corresponding resource release (closing the file, deallocating memory) should occur in the destructor. When an exception is thrown, destructors are automatically called, ensuring resources are always cleaned up, preventing leaks.