Programming Pandit

c/c++/c#/Javav/Python


Latest Update

Friday, October 23, 2026

Advanced Exception Handling: Derived Class Exceptions and Handling Options

Advanced Exception Handling: Derived Class Exceptions and Handling Options

 

Learning Objectives

After completing this module, students should be able to:

•      Explain how handlers match exceptions of derived classes and why the order of handlers matters.

•      Choose between specific handlers, general handlers, catch (...), rethrowing, and recovery.

•      Explain the difference between throw; and throw e; (object slicing).

•      Write custom exception classes and exception hierarchies.

•      Wrap one exception inside another with nested exceptions (C++11).

 

1. Handling Derived Class Exceptions

Exceptions can be objects of classes, and classes can be related by inheritance. A handler whose parameter is a base class will also catch objects of every class derived from it (publicly), because a derived object is-a base object. This allows one handler to deal with a whole family of related errors.

Title: An exception hierarchy: FileError, NetworkError and PaymentError derive from std::runtime_error, which derives from std::exception. - Description: An exception hierarchy: FileError, NetworkError and PaymentError derive from std::runtime_error, which derives from std::exception.

Figure 4.1: An exception hierarchy: FileError, NetworkError and PaymentError derive from std::runtime_error, which derives from std::exception.

1.1 Matching a derived exception

Program 4.1: A derived exception class and two handlers: derived first, then base.

#include <iostream>

#include <stdexcept>

#include <string>

using namespace std;

 

class FileError : public runtime_error

{

public:

    FileError(const string &message) : runtime_error(message) {}

};

 

int main()

{

    try {

        throw FileError("Unable to open the file");

    }

    catch (const FileError &e) {                 // most specific handler first

        cout << "FileError caught: " << e.what() << endl;

    }

    catch (const runtime_error &e) {             // base class handler

        cout << "Runtime error: " << e.what() << endl;

    }

    return 0;

}

 

Output:

FileError caught: Unable to open the file

 

1.      FileError derives from runtime_error, so a FileError object is also a runtime_error.

2.      The handlers are tried in order. catch (const FileError &) matches exactly and runs; the base handler is skipped.

3.      If a different runtime_error were thrown, the first handler would not match but the second one would.

 

1.2 Order of handlers: the most specific first

Important: Because the first matching handler is used, handlers for derived classes must be written before handlers for their base classes. If the base handler comes first, it catches the derived objects as well, and the derived handler can never run.

 

Program 4.2: Wrong order – the base handler is placed first.

#include <iostream>

#include <stdexcept>

using namespace std;

 

class FileError : public runtime_error

{

public:

    FileError(const char *m) : runtime_error(m) {}

};

 

int main()

{

    try {

        throw FileError("disk failure");

    }

    catch (const runtime_error &e) {             // base handler placed FIRST

        cout << "runtime_error handler: " << e.what() << endl;

    }

    catch (const FileError &e) {                 // unreachable

        cout << "FileError handler: " << e.what() << endl;

    }

    return 0;

}

 

GCC detects the mistake and warns at compile time:

p4_order_bad.cpp:19:5: warning: exception of type ‘FileError’ will be caught by earlier handler [-Wexceptions]

p4_order_bad.cpp:16:5: note: for type ‘std::runtime_error’

 

Output:

runtime_error handler: disk failure

 

The FileError handler is unreachable: the thrown object was received by the runtime_error handler. The warning (-Wexceptions, enabled by default) is a compiler diagnostic, not an error, so the program still builds – always read warnings.

1.3 How a handler is selected

Title: Handler selection: the first matching handler runs; otherwise the search continues outward. - Description: Handler selection: the first matching handler runs; otherwise the search continues outward.

Figure 4.2: Handler selection: the first matching handler runs; otherwise the search continues outward.

Program 4.3: Four handlers, from the most specific to the catch-all, and four different exceptions.

#include <iostream>

#include <stdexcept>

using namespace std;

 

class NetworkError : public runtime_error

{

public:

    NetworkError() : runtime_error("Network connection failed") {}

};

 

void test(int k)

{

    try {

        if (k == 1) throw NetworkError();

        if (k == 2) throw runtime_error("Generic runtime problem");

        if (k == 3) throw logic_error("Logic problem");

        throw 5;

    }

    catch (const NetworkError &e) {

        cout << "Specific handler : " << e.what() << endl;

    }

    catch (const runtime_error &e) {

        cout << "Runtime handler  : " << e.what() << endl;

    }

    catch (const exception &e) {

        cout << "General handler  : " << e.what() << endl;

    }

    catch (...) {

        cout << "Catch-all handler: unknown type" << endl;

    }

}

 

int main()

{

    for (int k = 1; k <= 4; k++)

        test(k);

    return 0;

}

 

Output:

Specific handler : Network connection failed

Runtime handler  : Generic runtime problem

General handler  : Logic problem

Catch-all handler: unknown type

 

Thrown object

Handler that runs

Reason

NetworkError

Specific handler

Exact match

runtime_error

Runtime handler

Exact match for the base class of NetworkError; NetworkError handler does not match

logic_error

General handler (exception)

logic_error derives from exception, not from runtime_error

int (5)

Catch-all

No class handler can accept an int

 

1.4 Catch by reference

A handler that takes a class object by value, catch (runtime_error e), makes a copy of only the base-class part of a derived object. This is called object slicing: the extra members and the overridden virtual functions of the derived object are lost. Always catch class-type exceptions by reference to const: catch (const runtime_error &e). This also avoids an unnecessary copy.

2. Exception Handling Options

When an exception has been caught, the handler may do one of several things. The right choice depends on what the handler knows about the problem.

Option

What the handler does

Typical use

Handle specifically

Separate catch for each important type

Different recovery actions for different errors

Handle generally

Catch a base class such as std::exception

One handler for a family of errors; report what()

Catch everything

catch (...)

Last line of defence, logging, clean-up

Rethrow

throw;

Do part of the work, let a caller finish

Recover and retry

Fix the cause and repeat the operation

Invalid user input, temporary resource failure

Translate or wrap

Throw a different, more meaningful exception

Hide low-level details from higher layers

Terminate safely

Log, release resources, exit

Unrecoverable errors

 

2.1 Specific handlers, general handlers and the catch-all

Programs 4.1 and 4.3 showed the first three options. A good design places specific handlers first, then a handler for the base class (usually const exception &), and, where necessary, catch (...) last. The catch-all handler cannot know the type of the exception, so it can only report that something went wrong, clean up and possibly rethrow.

2.2 Rethrowing an exception

A handler can pass the exception on to an outer handler with a throw; statement without an operand. The original exception object, with its real type and data, is passed on.

Program 4.4: Log the error in the inner function and let main() handle it.

#include <iostream>

#include <stdexcept>

using namespace std;

 

void process()

{

    try {

        throw runtime_error("Database connection failed");

    }

    catch (const exception &e) {

        cout << "Logged in process(): " << e.what() << endl;

        // pass the same exception outward

        throw;

    }

}

 

int main()

{

    try {

        process();

    }

    catch (const exception &e) {

        cout << "Handled in main(): " << e.what() << endl;

    }

    return 0;

}

 

Output:

Logged in process(): Database connection failed

Handled in main(): Database connection failed

 

Title: Function B logs the exception and rethrows the same object to main(). - Description: Function B logs the exception and rethrows the same object to main().

Figure 4.3: Function B logs the exception and rethrows the same object to main().

throw; versus throw e;

Writing throw e; inside a handler is different: it creates a new exception object copied from the variable e, whose declared type may be a base class. If e was declared as a reference to a base class, the copy is sliced and the derived type is lost.

Program 4.5: The effect of throw; and throw e; on a derived exception.

#include <iostream>

#include <stdexcept>

#include <typeinfo>

using namespace std;

 

class PaymentError : public runtime_error

{

public:

    PaymentError(const char *m) : runtime_error(m) {}

};

 

void rethrowCorrectly()

{

    try {

        throw PaymentError("Payment declined");

    }

    catch (exception &e) {

        throw;                      // the original object is passed on

    }

}

 

void rethrowWithCopy()

{

    try {

        throw PaymentError("Payment declined");

    }

    catch (exception &e) {

        throw e;                    // a NEW exception object is copied from e

    }

}

 

int main()

{

    try { rethrowCorrectly(); }

    catch (const PaymentError &e) {

        cout << "throw;   -> PaymentError caught: " << e.what() << endl;

    }

    catch (const exception &e) {

        cout << "throw;   -> only exception caught" << endl;

    }

 

    try { rethrowWithCopy(); }

    catch (const PaymentError &e) {

        cout << "throw e; -> PaymentError caught: " << e.what() << endl;

    }

    catch (const exception &e) {

        cout << "throw e; -> only exception caught (object sliced)" << endl;

    }

    return 0;

}

 

Output:

throw;   -> PaymentError caught: Payment declined

throw e; -> only exception caught (object sliced)

 

Important: To pass an exception on, always use throw;, never throw e;.

 

2.3 Recovering from an exception

Sometimes the program can correct the cause of the problem and try again. A typical case is invalid input. The try-catch must then be inside a loop, and the cause (for example, the failed state of cin) must be cleared before the next attempt.

Program 4.6: Keep asking for the age until the input is valid.

#include <iostream>

#include <limits>

#include <stdexcept>

using namespace std;

 

int readAge()

{

    int age;

    cout << "Enter age: ";

    if (!(cin >> age)) {

        // reset the stream state

        cin.clear();

        // discard the bad line

        cin.ignore(numeric_limits<streamsize>::max(), '\n');

        throw runtime_error("Input is not a number");

    }

    if (age < 0 || age > 120)

        throw invalid_argument("Age must be between 0 and 120");

    return age;

}

 

int main()

{

    int age = -1;

    bool valid = false;

 

    // keep asking until the input is valid

    while (!valid) {

        try {

            age = readAge();

            valid = true;

        }

        catch (const invalid_argument &e) {

            cout << "Invalid value: " << e.what() << endl;

        }

        catch (const runtime_error &e) {

            cout << "Bad input: " << e.what() << endl;

        }

    }

    cout << "Valid age: " << age << endl;

    return 0;

}

 

Output for the inputs abc, -5 and 25:

Enter age: abc

Bad input: Input is not a number

Enter age: -5

Invalid value: Age must be between 0 and 120

Enter age: 25

Valid age: 25

 

•      For the text abc the extraction from cin fails. cin.clear() resets the error state and cin.ignore(...) removes the bad line; the function then throws runtime_error.

•      For -5 the number is read but is outside the valid range, so invalid_argument is thrown.

•      In both cases the loop continues and asks again; valid becomes true only when readAge() returns without throwing.

•      The handler for invalid_argument is written before the one for runtime_error because both derive from exception but are unrelated to each other; the order here is a matter of clarity.

 

2.4 Translating and wrapping exceptions

A low-level error such as “disk read failed” is meaningless for the code that only wanted to load a configuration. A handler can therefore throw a more meaningful exception, keeping the original one inside it. The C++11 facilities std::throw_with_nested() and std::rethrow_if_nested() do this.

Program 4.7: Wrapping an exception and printing the whole chain.

#include <iostream>

#include <exception>

#include <stdexcept>

#include <string>

using namespace std;

 

void lowLevel()

{

    throw runtime_error("disk read failed");

}

 

void highLevel()

{

    try {

        lowLevel();

    }

    catch (...) {

        // wrap the original exception inside a more meaningful one

        throw_with_nested(runtime_error("cannot load the configuration"));

    }

}

 

/* Print an exception and, recursively, the exceptions nested inside it */

void printAll(const exception &e, int level = 0)

{

    cout << string(level * 2, ' ') << e.what() << endl;

    try {

        rethrow_if_nested(e);

    }

    catch (const exception &inner) {

        printAll(inner, level + 1);

    }

}

 

int main()

{

    try {

        highLevel();

    }

    catch (const exception &e) {

        printAll(e);

    }

    return 0;

}

 

Output:

cannot load the configuration

  disk read failed

 

Version Note: Nested exceptions (throw_with_nested, rethrow_if_nested, std::nested_exception) and std::exception_ptr (current_exception, rethrow_exception) were added in C++11. They are not available in C++98/03; Program 4.7 does not compile with -std=c++98.

 

3. Custom Exception Classes and Hierarchies

Throwing a plain int or string tells the handler very little. A user-defined class can carry an error message, an error code and any other useful data, and can be placed in a hierarchy so that handlers can be as specific or as general as needed.

Guidelines.

•      Derive custom exceptions from std::exception (or from runtime_error or logic_error) so that generic catch (const exception &) handlers work for them.

•      Override what() to return a description of the error.

•      Throw objects by value and catch them by reference to const.

•      Make a base class for the errors of one library or program, and derive specific errors from it.

 

Program 4.8: A hierarchy AppError → DatabaseError and NetworkError with error codes.

#include <iostream>

#include <string>

using namespace std;

 

/* A base class for all errors of an application. It derives from std::exception

   so that generic handlers still work. */

class AppError : public exception

{

    string message;

    int code;

 

public:

    AppError(const string &m, int c) : message(m), code(c) {}

    const char *what() const noexcept override { return message.c_str(); }

    int errorCode() const { return code; }

};

 

class DatabaseError : public AppError

{

public:

    DatabaseError(const string &m) : AppError("Database: " + m, 101) {}

};

 

class NetworkError : public AppError

{

public:

    NetworkError(const string &m) : AppError("Network: " + m, 202) {}

};

 

void run(int k)

{

    try {

        if (k == 1) throw DatabaseError("connection lost");

        if (k == 2) throw NetworkError("timeout");

        throw AppError("unknown failure", 999);

    }

    catch (const DatabaseError &e) {

        cout << "DB handler     -> " << e.what() << " (code " << e.errorCode()

            << ")" << endl;

    }

    catch (const AppError &e) {                     // any other application error

        cout << "AppError handler -> " << e.what() << " (code " << e.errorCode()

            << ")" << endl;

    }

}

 

int main()

{

    run(1);

    run(2);

    run(3);

    return 0;

}

 

Output:

DB handler     -> Database: connection lost (code 101)

AppError handler -> Network: timeout (code 202)

AppError handler -> unknown failure (code 999)

 

•      AppError overrides what() and adds an error code. DatabaseError and NetworkError add a prefix to the message and fix their code.

•      In run(1) the specific DatabaseError handler runs. For NetworkError and for a plain AppError there is no specific handler, so the AppError handler (base class) receives them. This is the benefit of the hierarchy: a new kind of error needs no new handler unless it needs special treatment.

 

Version Note: Signature of what(). In C++11 and later it is written const char *what() const noexcept override. Before C++11 it was virtual const char *what() const throw(), using a dynamic exception specification.

throw() as a specification is deprecated since C++11 and removed in C++20 (Module 5). New code should use noexcept; the keyword override also requires C++11.

 

4. Good Practices

Practice

Reason

Throw by value, catch by const reference

No slicing, no unnecessary copies, no pointer ownership problems

Derive from std::exception

Generic handlers and what() work for your classes

Order handlers from derived to base, then catch (...)

The first match wins

Use throw; to rethrow

Preserves the original type

Do not throw pointers to local objects

The object no longer exists when the handler runs

Do not let exceptions leave destructors

A second exception during unwinding terminates the program (Module 6)

Catch only what you can handle

Let other exceptions propagate to code that knows more

 

5. Quick Revision

•      A base-class handler catches derived exceptions; derived handlers must come first.

•      Catch class exceptions by const reference; throw; keeps the original object, throw e; may slice it.

•      Options: specific handler, general handler, catch (...), rethrow, recover (loop), wrap (nested exceptions), terminate safely.

•      Custom exceptions derive from std::exception and override what() (noexcept in C++11 and later).

 

6. Important Examination Questions

Short-answer questions

1.      How does a base class handler treat objects of derived classes?

2.      Why must derived-class handlers be placed before base-class handlers?

3.      What is object slicing? How is it avoided in exception handling?

4.      Differentiate between throw; and throw e;.

5.      Why should exception classes override what()?

6.      What is the purpose of nested exceptions?

 

Long-answer and programming questions

1.      Explain handling of derived class exceptions with a program and the correct order of handlers.

2.      Explain the options available for handling an exception, with an example of each.

3.      Write a program that defines an exception hierarchy and catches it with specific and general handlers.

4.      Write a program that recovers from invalid input using exceptions and a loop.

5.      Explain rethrowing of exceptions with a program.

6.      Write a custom exception class with a message and an error code.

 

 


No comments:

Post a Comment