C++ Learning Manual – Exception Handling: Custom Exceptions

In the previous sections, we learned about the exception types provided by the C++ standard library (such as <span>std::runtime_error</span>, <span>std::out_of_range</span>, etc.). However, in complex projects, using standard exceptions often fails to clearly and accurately describe specific errors encountered in the program. At this point, custom exceptions become a key technology for building robust, maintainable, and easily debuggable C++ applications.

Why Do We Need Custom Exceptions?

  1. Express specific domain errors: For example, in a banking application, you can define <span>InsufficientFundsException</span>, which is much clearer than the generic <span>std::runtime_error("Withdrawal amount too large")</span>.
  2. Carry rich error information: Custom exception classes can contain multiple data members to store various contextual information related to the error (such as error codes, operation IDs, timestamps, etc.).
  3. Fine-grained exception catching and handling: <span>catch</span> blocks can precisely catch specific types of exceptions, allowing for more targeted error recovery logic.
  4. Improve code readability and maintainability: The names of exception classes serve as documentation, clearly indicating potential issues.

How to Create Custom Exception Classes?

In C++, creating a custom exception class is straightforward: define a new class that inherits from a standard exception class (usually <span>std::exception</span> or one of its derived classes).

Basic Steps:
  1. Include necessary header files:

    #include <exception> // For std::exception
    #include <string>    // For std::string
    
  2. Derive from <span>std::exception</span>: Your new exception class should publicly inherit from <span>std::exception</span> or one of its derived classes (like <span>std::runtime_error</span>). Inheriting from <span>std::runtime_error</span> is often a better choice because it already has built-in capabilities for storing string information.

  3. Override the <span>what()</span> method: The <span>std::exception</span> base class defines a virtual function <span>virtual const char* what() const noexcept;</span>. This function is intended to return a C-style string describing the exception. In your custom exception class, you must override this function to return your own error message.

Example 1: Inheriting from <span>std::exception</span>

#include <exception>
#include <string>

class MyCustomException : public std::exception {
private:
    std::string message; // Used to store detailed error information

public:
    // Constructor that receives error information
    explicit MyCustomException(const std::string& msg) : message(msg) {}

    // Override what() method to return stored error information
    const char* what() const noexcept override {
        return message.c_str();
    }
};
  • Usage example:
#include <iostream>

void riskyFunction(int value) {
    if (value < 0) {
        throw MyCustomException("Input value cannot be negative!");
    }
    // ... normal logic
}

int main() {
    try {
        riskyFunction(-5);
    } catch (const MyCustomException& e) {
        std::cerr << "Caught my custom exception: " << e.what() << std::endl;
    } catch (const std::exception& e) {
        // Catch other standard exceptions
        std::cerr << "Caught standard exception: " << e.what() << std::endl;
    }
    return 0;
}
// Output: Caught my custom exception: Input value cannot be negative!
Example 2: A More Recommended Approach — Inheriting from <span>std::runtime_error</span>

<span>std::runtime_error</span> class internally maintains a string message and overrides the <span>what()</span> method. Directly inheriting it can make the code cleaner.

#include <stdexcept> // Include std::runtime_error

class FileOpenException : public std::runtime_error {
public:
    // Constructor directly calls the base class std::runtime_error constructor
    explicit FileOpenException(const std::string& filename)
        : std::runtime_error("Failed to open file: " + filename) {}
};
  • Usage example:
#include <fstream>

void openFile(const std::string& filename) {
    std::ifstream file(filename);
    if (!file.is_open()) {
        throw FileOpenException(filename);
    }
    // ... file operations
}

int main() {
    try {
        openFile("nonexistent.txt");
    } catch (const FileOpenException& e) {
        std::cerr << e.what() << std::endl; // Output: Failed to open file: nonexistent.txt
    }
    return 0;
}

Creating More Complex Custom Exceptions

You can add any member data to the exception class that helps with debugging and error handling.

class DatabaseQueryException : public std::runtime_error {
private:
    int errorCode;
    std::string failedQuery;

public:
    DatabaseQueryException(int code, const std::string& query, const std::string& message)
        : std::runtime_error(message), errorCode(code), failedQuery(query) {}

    int getErrorCode() const { return errorCode; }
    const std::string& getFailedQuery() const { return failedQuery; }
};

// Usage
try {
    // ... execute database query
    throw DatabaseQueryException(1064, "SELECT * FROM non_existent_table", "Syntax error in SQL query");
} catch (const DatabaseQueryException& e) {
    std::cerr << "DB Error (" << e.getErrorCode() << "): " << e.what() << "\n";
    std::cerr << "Failed query: " << e.getFailedQuery() << std::endl;
}
// Output:
// DB Error (1064): Syntax error in SQL query
// Failed query: SELECT * FROM non_existent_table

Best Practices

  1. Prefer inheriting from <span>std::runtime_error</span>: It saves you the trouble of managing string messages yourself and is the most commonly used and convenient base class for creating custom exceptions.
  2. Catch by reference: Always catch exceptions by <span>const</span> reference (like <span>catch (const MyException& e)</span>) to avoid unnecessary object slicing and copying.
  3. Maintain <span>noexcept</span> specification: When overriding the <span>what()</span> method, keep its <span>noexcept</span> specification, as it should not throw exceptions.
  4. Design a clear exception hierarchy: For large projects, you can create a base exception class that inherits from <span>std::exception</span>, and then let other more specific exception classes inherit from it, forming a clear hierarchy.
  5. Provide meaningful error messages: Include enough context in the exception messages to facilitate quick problem identification.

Conclusion

Custom exceptions are a powerful and flexible feature of the C++ exception handling mechanism. By deriving from standard exception classes, you can create semantically clear, information-rich, and highly targeted error types, greatly enhancing the quality and debuggability of your code. Remember, a good exception class serves as excellent documentation.

Leave a Comment