Exception Handling Mechanisms in C++

Scenarios Requiring Exception Handling

Runtime anomalies often occur during program execution, such as:
  • Division operations where the divisor is zero.
  • Invalid user input, for instance, a negative value for age.
  • Memory allocation failure using the new operator due to insufficient space.
  • Array index out of bounds or attempting to open a non-existent file.
Without proper detection and management, these abnormal situations frequently lead to application crashes.

Fundamentals of Exception Handling

It is crucial to understand that an exception occurring within a function can either be resolved internally within that function or delegated to its caller for resolution. The standard syntax involves a try block followed by one or more catch handlers.
try {
    // Code segment that may throw exceptions
}
catch (ExceptionType1 ex1) {
    // Handling logic for ExceptionType1
}
catch (ExceptionType2 ex2) {
    // Handling logic for ExceptionType2
}
The try block contains the code where potential exceptions are anticipated, including throw statements. A throw statement is followed by an expression; the type of this expression can be a primitive type or a class object, often containing details about the error. If we view the catch block as a function, the exception type acts as the parameter, and the expression in throw acts as the argument. The mechanism functions similarly to a switch statement. Once a throw expression is caught by a matching catch block, subsequent catch blocks are skipped, and execution continues after the entire handler sequence.

Basic Exception Handling Example

#include <iostream>
using namespace std;

int main() {
    double dividend, divisor;
    cin >> dividend >> divisor;

    try {
        cout << "Starting calculation." << endl;
        
        if (divisor == 0) {
            throw -1; // Throws an integer exception
        } else {
            cout << dividend / divisor << endl;
        }
        
        cout << "Calculation finished." << endl;
    }
    catch (double val) {
        cout << "Caught double exception: " << val << endl;
    }
    catch (int err) {
        cout << "Caught int exception: " << err << endl;
    }

    cout << "Program complete." << endl;
    return 0;
}
Output (normal case):
10 2
Starting calculation.
5
Calculation finished.
Program complete.
When the divisor is not zero, no exception is thrown. The program executes the entire try block and skips all catch blocks. Output (exception case):
10 0
Starting calculation.
Caught int exception: -1
Program complete.

Catching All Exceptions

To capture any type of exception regardless of its specific type, an ellipsis ... can be used in the catch block:
catch (...) {
    // Handle any exception
}
#include <iostream>
using namespace std;

int main() {
    double val1, val2;
    cin >> val1 >> val2;

    try {
        cout << "Starting process." << endl;
        
        if (val2 == 0) {
            throw -1; // Integer exception
        } else if (val1 == 0) {
            throw -1.0; // Double exception
        } else {
            cout << val1 / val2 << endl;
        }
        
        cout << "Process finished." << endl;
    }
    catch (double d) {
        cout << "Caught double: " << d << endl;
    }
    catch (...) {
        cout << "Caught unknown exception" << endl;
    }

    cout << "End of main." << endl;
    return 0;
}
If val1 is 0, a double exception is thrown. Although both catch(double) and catch(...) could match it, the first specific match is chosen, similar to function overloading resolution. Since catch(...) matches everything, it should be placed last to avoid shadowing specific handlers.

Exception Flow and Multiple Throws

If a try block contains multiple throw statements, execution stops at the first one thrown. Even if no matching catch exists locally, the program will not execute subsequent throw statements or code below them in the try block. The control flow transfers immediately to the handler or calls terminate if unhandled.
try {
    throw 100;
    throw 3.14; // This line is unreachable
}
catch (int x) {
    cout << "Integer: " << x << endl;
}
catch (double y) {
    cout << "Double: " << y << endl;
}
If an exception is not caught by any local catch block, it propagates up the call stack to the caller. If it reaches the top level (main) without being caught, the standard library function terminate is called, typically invoking abort to end the program.
#include <iostream>
#include <string>
using namespace std;

class CustomException {
public:
    string message;
    CustomException(string msg) : message(msg) {}
};

double PerformDivision(double x, double y) {
    if (y == 0)
        throw CustomException("Division by zero error");
    cout << "Inside Division Function" << endl;
    return x / y;
}

int CalculateTax(int income) {
    try {
        if (income < 0)
            throw -1;
        cout << "Calculating tax..." << endl;
    }
    catch (int errorCode) {
        cout << "Invalid income (negative)" << endl;
    }
    cout << "Tax calculated." << endl;
    return income * 0.15;
}

int main() {
    double result = 1.2;
    try {
        CalculateTax(-500);
        // PerformDivision throws an exception not caught inside the function
        result = PerformDivision(10, 0); 
        cout << "End of try block" << endl;
    }
    catch (CustomException e) {
        cout << e.message << endl;
    }
    
    cout << "Result: " << result << endl;
    cout << "Execution finished." << endl;
    return 0;
}

Rethrowing Exceptions

Sometimes a function may catch an exception to perform partial cleanup or logging but still needs to notify the caller that an error occurred. This is done by using throw; without an argument inside a catch block.
#include <iostream>
#include <string>
using namespace std;

int ProcessTax(int salary) {
    try {
        if (salary < 0)
            throw string("Negative salary provided");
        cout << "Processing tax calculation..." << endl;
    }
    catch (string err) {
        cout << "ProcessTax Error: " << err << endl;
        throw; // Rethrow the caught exception to the caller
    }
    cout << "Tax processed." << endl;
    return salary * 0.15;
}

int main() {
    try {
        ProcessTax(-100);
        cout << "End of try block" << endl;
    }
    catch (string msg) {
        cout << "Main caught: " << msg << endl;
    }
    cout << "Done." << endl;
    return 0;
}

Exception Specifications

To improve code readability and allow callers to anticipate potential errors, C++ allows specifying the types of exceptions a function might throw. This is done using an exception specification list in the function declaration or definition.
// Declaration
void TaskFunction() throw (int, double, CustomClassA, CustomClassB);

// Definition
void TaskFunction() throw (int, double, CustomClassA, CustomClassB) {
    // Implementation
}
This indicates that TaskFunction may only throw exceptions of type int, double, CustomClassA, or CustomClassB. If both declaration and definition have specifications, they must match. An empty list indicates the function promises not to throw any exceptions:
void SafeFunction() throw ();
If no specification is provided, the function may throw any type of exception. Note that in C++11 and later, dynamic exception specifications (except throw() which is equivalent to noexcept) are deprecated, but older compilers might still enforce them at runtime rather than compile time.

Standard C++ Exception Classes

The C++ Standard Library provides a hierarchy of exception classes derived from std::exception. Common classes include bad_typeid, bad_cast, bad_alloc, ios_base::failure, and out_of_range. These classes typically include a what() member function that returns a description of the error. Using these classes requires including <stdexcept>.

1. bad_typeid

Thrown when the typeid operator is applied to a polymorphic class pointer that is a null pointer.
#include <iostream>
#include <typeinfo>

class Base {
public:
    virtual ~Base() {}
};

class Derived : public Base {};

int main() {
    Base* ptr = nullptr;
    try {
        std::cout << typeid(*ptr).name() << std::endl;
    }
    catch (std::exception& e) {
        std::cerr << "Error: " << e.what() << std::endl;
    }
    return 0;
}

2. bad_cast

Thrown by dynamic_cast when a reference cast fails (e.g., trying to cast a base class reference to a derived class reference when the object is not of the derived type).
#include <iostream>
#include <stdexcept>
using namespace std;

class Base {
    virtual void dummy() {}
};

class Derived : public Base {
public:
    void show() { cout << "Derived method" << endl; }
};

void ProcessObject(Base& b) {
    try {
        Derived& d = dynamic_cast<Derived&>(b);
        d.show();
    }
    catch (bad_cast& e) {
        cerr << e.what() << endl;
    }
}

int main() {
    Base b;
    ProcessObject(b);
    return 0;
}

3. bad_alloc

Thrown by the new operator if memory allocation fails.
#include <iostream>
#include <new>
using namespace std;

int main() {
    try {
        // Attempt to allocate a very large amount of memory
        char* buffer = new char[0xFFFFFFFFFFFFFFF];
    }
    catch (bad_alloc& e) {
        cerr << "Allocation failed: " << e.what() << endl;
    }
    return 0;
}

4. out_of_range

Thrown by methods like std::vector::at() or std::string::at() when an index is out of bounds. Unlike operator[], which leads to undefined behavior on out-of-bounds access, at() performs bounds checking and throws this exception.
#include <iostream>
#include <vector>
#include <string>
#include <stdexcept>
using namespace std;

int main() {
    vector<int> numbers(5);
    try {
        numbers.at(10) = 5; // Throws out_of_range
    }
    catch (out_of_range& e) {
        cerr << e.what() << endl;
    }

    string text = "Hello";
    try {
        char c = text.at(10); // Throws out_of_range
    }
    catch (out_of_range& e) {
        cerr << e.what() << endl;
    }
    return 0;
}

Tags: C++ Exception Handling programming software development standard library

Posted on Mon, 20 Jul 2026 17:26:08 +0000 by lm_a_dope