Skip to Content
Course content

99: Custom Exception Hierarchies

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I often see developers approach custom exceptions by creating a flat list of classes. They’ll make a FileNotFoundException, a DiskFullException, and a PermissionDeniedException, all inheriting directly from std::exception. On the surface, it looks organized. You have a specific type for every specific problem. But in a real-world production system, this is a maintenance nightmare.

The Flat-List Trap: Thinking every error needs a standalone class

When you create a flat list of exceptions, you force the person using your code to either write a massive block of catch statements for every single possibility, or give up and catch std::exception. Catching std::exception is too broad—you might accidentally swallow a std::bad_alloc or some other system error you weren't prepared to handle.

// The "Flat" approach - Don't do this for large systems
try {
    myFileSystem.save(data);
} catch (const FileNotFoundException& e) { 
    /* handle it */ 
} catch (const DiskFullException& e) { 
    /* handle it */ 
} catch (const PermissionDeniedException& e) { 
    /* handle it */ 
}
// What if I just want to handle ANY file system error? 
// I have to list them all here.

This approach fails because it ignores the most powerful tool in C++: polymorphism. If you have twenty different types of IO errors, you don't want to write twenty catch blocks every time you call a function.

Using Polymorphism to Categorize Failures

The right way to do this is to build a hierarchy. I usually suggest creating a base exception class for your specific module or library that inherits from std::runtime_error. Why std::runtime_error? Because it already handles the string message storage for you, saving you from writing a custom constructor just to pass a message up to std::exception.

By grouping your exceptions, you give the caller a choice: they can be surgical and catch a specific error, or they can be general and catch the category base class.

#include <stdexcept>
#include <string>
#include <iostream>

// The base for all our database-related errors
class DatabaseException : public std::runtime_error {
public:
    explicit DatabaseException(const std::string& msg) 
        : std::runtime_error(msg) {}
};

// Specialized errors inherit from the category base
class ConnectionTimeoutException : public DatabaseException {
public:
    explicit ConnectionTimeoutException() 
        : DatabaseException("Database connection timed out.") {}
};

class QuerySyntaxException : public DatabaseException {
public:
    explicit QuerySyntaxException(const std::string& query) 
        : DatabaseException("Syntax error in query: " + query) {}
};

void executeQuery(const std::string& query) {
    if (query.empty()) throw QuerySyntaxException(query);
    if (/* network fails */ true) throw ConnectionTimeoutException();
}

int main() {
    try {
        executeQuery("");
    } catch (const ConnectionTimeoutException& e) {
        std::cout << "Retrying connection..." << std::endl;
    } catch (const DatabaseException& e) {
        // This catches QuerySyntaxException AND any other DatabaseException
        // that isn't a timeout.
        std::cerr << "Database error occurred: " < e.what() << std::endl;
    }
}

Notice how the catch (const DatabaseException& e) block acts as a "catch-all" for that specific domain. I find this is the sweet spot for API design. You provide enough granularity for the power user to handle specific edge cases, but you don't force every user to write a novel of catch blocks just to keep the app from crashing.

One quick tip: always inherit publicly and always catch by reference. If you catch by value, you'll run into "slicing," where the derived part of your exception is stripped away, and you lose the specific type information you worked so hard to build.




📋 Practical Task

Implement a Hierarchical Network Protocol Error System

You are building a network library. Instead of using generic exceptions, implement a custom hierarchy that allows a user to distinguish between different types of network failures while still being able to handle all "Network Errors" collectively.

Requirements:

  • Create a base class NetworkException that inherits from std::runtime_error.
  • Create two derived classes: PacketLossException and InvalidHeaderException, both inheriting from NetworkException.
  • Write a function processPacket(int packetId) that:
    • Throws PacketLossException if packetId is negative.
    • Throws InvalidHeaderException if packetId is greater than 1000.
  • In your main function, wrap the call to processPacket in a try-catch block. Implement two catch handlers:
    1. One specifically for PacketLossException to print "Requesting retransmission...".
    2. One for the base NetworkException to catch any other network-related errors (including InvalidHeaderException) and print the error message using .what().
Rating
0 0

There are no comments for now.

to be the first to leave a comment.