Intermediate 12 min

Custom Exceptions

Create custom exception classes to provide domain-specific error information

Learn how to create custom exception classes that provide meaningful, domain-specific error information for your applications.

A Simple Example

#include <iostream>
#include <exception>
#include <string>

class FileNotFoundException : public std::exception {
    std::string filename;
    std::string message;

public:
    explicit FileNotFoundException(const std::string& file)
        : filename{file}
        , message{"File not found: " + file} {}

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

    const std::string& getFilename() const { return filename; }
};

void openFile(const std::string& filename) {
    if (filename.empty()) {
        throw FileNotFoundException{filename};
    }
    std::cout << "Opening: " << filename << "\n";
}

int main() {
    try {
        openFile("");
    } catch (const FileNotFoundException& e) {
        std::cerr << "Error: " << e.what() << "\n";
        std::cerr << "Filename was: '" << e.getFilename() << "'\n";
    }
    return 0;
}

Breaking It Down

Inheriting from std::exception

  • What it does: Makes your exception catchable by catch(const std::exception&)
  • Must override: The what() method to return error message
  • Best practice: Always inherit from std::exception or one of its derived classes
  • Remember: This creates a unified exception hierarchy in your code

The what() method with noexcept

  • Signature: const char* what() const noexcept override
  • Why noexcept: If what() throws during exception handling, program terminates
  • Return type: Must be const char*, typically from std::string::c_str()
  • Remember: Store the message as a member to ensure lifetime

Storing exception context

  • Add member variables: Store additional context like filename, error codes, etc.
  • Provide accessors: Let callers extract specific information
  • Use explicit constructors: Prevent accidental implicit conversions
  • Remember: More context helps with debugging and error handling

Building exception hierarchies

  • Create base classes: Define domain-specific base exceptions
  • Derive specific errors: Make specialized exceptions inherit from your base
  • Catch flexibility: Callers can catch specific or general exception types
  • Remember: Hierarchy mirrors your domain model and error categories

Why This Matters

  • Standard exceptions like std::runtime_error are generic and lack context about what went wrong in your specific application.
  • Custom exceptions let you encode specific error conditions in your type system, making it easier to handle different failures differently.
  • A banking app might have InsufficientFundsException vs AccountClosedException - each requiring different handling logic and user messages.

Critical Insight

Exception hierarchies mirror your domain model. Create a base exception class for your module (like ValidationException), then derive specific exceptions from it (like InvalidEmailException, PasswordTooShortException).

This lets callers catch either specific errors (to show targeted messages) or the whole category (for generic error handling) - giving them control over granularity. It is like organizing errors into folders and subfolders.

Best Practices

Always inherit from std::exception: This makes your exceptions compatible with standard exception handling and catchable by generic catch blocks.

Mark what() as noexcept: This prevents program termination if the method throws during exception handling.

Store the message as a member: Do not return pointers to temporary strings from what(). Store the message in a member variable.

Use explicit constructors: Single-argument constructors should be explicit to prevent implicit conversions that could create exceptions accidentally.

Provide context accessors: Add getter methods for error-specific information like filenames, error codes, or invalid values.

Common Mistakes

Forgetting noexcept on what(): The what() method must be noexcept or your program can terminate if it throws during exception handling.

Returning temporary string pointers: Never return c_str() from a temporary string. Store the message as a member.

Not using explicit constructors: Single-argument constructors should be explicit to prevent implicit conversions.

Catching by value: Always catch exceptions by const reference to avoid slicing and unnecessary copies.

Debug Challenge

This custom exception has a bug in the what() method signature. Click the highlighted line to fix it:

1 class DatabaseException : public std::exception {
2 std::string message;
3 public:
4 explicit DatabaseException(const std::string& msg)
5 : message{msg} {}
6
7 const char* what() const override {
8 return message.c_str();
9 }
10 };

Quick Quiz

  1. Why should what() be marked noexcept?
To prevent termination if what() throws during exception handling
For performance optimization
It is optional, just good style
  1. What is the benefit of exception hierarchies?
They make code run faster
They allow catching specific or general error categories
They reduce binary size
  1. Why use explicit on exception constructors?
To make exceptions faster
It is required by the standard
To prevent accidental implicit conversions

Practice Playground

Time to try out what you just learned! Play with the example code below, experiment by making changes and running the code to deepen your understanding.

Lesson Progress

  • Fix This Code
  • Quick Quiz
  • Practice Playground - run once