Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 60

C++ Exception Handling

Recommended resources

Hello! Welcome to the tenth module of our course, where we'll focus on C++ Error Handling and Build Systems.

In our last module, we explored powerful modern C++ features like smart pointers, move semantics, templates, and lambda expressions, all aimed at writing more robust, efficient, and generic code. The RAII principle, in particular, showed us how to manage resources automatically to prevent leaks.

Now, we'll address a question that naturally follows: What happens when things go wrong at runtime? This lesson introduces C++'s primary mechanism for handling such situations: exceptions. Your goal is to learn how to handle errors using C++ exceptions with try, catch, and throw.

Since you're familiar with exception handling from Python (try...except), you already have a high-level understanding of the concept. In C++, the mechanism is similar but has its own specific rules and idioms, which are crucial for writing resilient systems, especially in areas like desktop and embedded applications where stability is key.

1. Why Do We Need Exceptions?

Before C++ had exceptions, error handling was done C-style, primarily using return codes or global error variables (like errno). A function would return a special value (e.g., -1 or nullptr) to indicate failure, and the caller was responsible for checking it.

This approach has several drawbacks:

  • Cluttered Code: The main logic of your program becomes interleaved with if statements for error checking, making it hard to read.
  • Easy to Ignore: A developer can easily forget to check the return code, leading to bugs where the program continues with invalid data.
  • Limited Information: An error code is often just a number and doesn't provide rich context about what went wrong.
  • The Constructor Problem: Constructors don't have a return type. How can a constructor report that it failed to create an object?

To see a clear comparison and understand why exceptions provide a cleaner, more robust alternative, the C++ FAQ offers a great explanation.

Exceptions and Error Handling, C++ FAQ

The Standard C++ FAQ provides a compelling rationale for using exceptions. It contrasts them with return codes and highlights how they lead to simpler, cleaner code.

Please read the following sections from this page: 'Why use exceptions?': This section introduces the core benefits. 'How do exceptions simplify my function return type and parameter types?': This shows a powerful before-and-after example with operator overloading. 'What does it mean that exceptions separate the “good path” from the “bad path”?': This illustrates how exceptions de-clutter your main application logic. Focus on the code examples that contrast the messy, nested if statements of return-code style with the clean try/catch structure of exception-based code.

As the article argues, exceptions allow you to separate the "happy path" (your main logic) from the "error path," making your code significantly more readable and maintainable.


2. The Mechanics: try, catch, and throw

The C++ exception handling mechanism revolves around three keywords that work together:

  1. throw: When your code encounters an unrecoverable error, it throws an exception. This is like raising an alarm. You can technically throw any type of value (an int, a const char*, etc.), but best practice is to throw an object of a class that inherits from std::exception.
  2. try: You place code that might throw an exception inside a try block. This tells the compiler, "I'm aware that an error might happen here, and I'm prepared to handle it."
  3. catch: Immediately following a try block, you write one or more catch blocks. Each catch block is a handler for a specific type of exception. If an exception is thrown inside the try block, the program looks for a catch block that matches the exception's type.

This video provides a great visual introduction to the syntax and basic flow.

Exception Handling - C++ Tutorial For Beginners #18

This tutorial from NeuralNine walks through the basic syntax of throwing and catching exceptions with a simple division-by-zero example.

Please watch from 02:12 to 06:10. This segment covers: How to use the throw keyword to signal an error (02:12 - 04:42). How to wrap the fallible code in a try block and handle the error in a catch block (04:42 - 06:10).


3. Stack Unwinding and RAII

When an exception is thrown, the normal flow of execution stops. The program immediately begins a process called stack unwinding. It exits the current function, destroying all local objects, and moves up the call stack to the function that called it. It repeats this process—exiting functions and destroying local objects—until it finds an enclosing try...catch block that can handle the exception.

C++ Exception Handling: Stack Unwinding
This image illustrates stack unwinding. The top flow shows normal function calls (main -> f_a -> f_b -> f_c). The bottom flow shows what happens when f_c throws an exception: control bypasses the return paths of f_c, f_b, and f_a, jumping directly to the `catch` block in `main`, destroying local objects in each stack frame along the way.

This is where the RAII (Resource Acquisition Is Initialization) principle you learned about becomes incredibly powerful. Because stack unwinding guarantees that destructors of local objects are called, resources managed by RAII objects (like std::unique_ptr, std::vector, or std::fstream) are automatically and correctly cleaned up. This synergy is a cornerstone of modern C++ error handling.

Let's look at a complete, idiomatic example:




#include <iostream>
#include <stdexcept> // For std::invalid_argument
#include <string>

// A function that might fail
double divide(double numerator, double denominator) {
    if (denominator == 0.0) {
        // Throw an exception of a standard type.
        throw std::invalid_argument("Denominator cannot be zero.");
    }
    return numerator / denominator;
}

int main() {
    std::cout << "Attempting to calculate 10.0 / 2.0\n";
    try {
        double result = divide(10.0, 2.0);
        std::cout << "Result: " << result << std::endl;
    } catch (const std::invalid_argument& e) {
        // This block is skipped because no exception was thrown.
        std::cerr << "Caught an error: " << e.what() << std::endl;
    }

    std::cout << "\nAttempting to calculate 10.0 / 0.0\n";
    try {
        // This call will throw an exception.
        double result = divide(10.0, 0.0);
        // This line will never be reached.
        std::cout << "Result: " << result << std::endl;
    } catch (const std::invalid_argument& e) {
        // Control jumps here.
        // e.what() returns the message we provided in the throw statement.
        std::cerr << "Caught an error: " << e.what() << std::endl;
    }

    std::cout << "\nProgram execution continues normally after handling the exception." << std::endl;

    return 0;
}

Notice that the program doesn't crash. The catch block handles the error gracefully, and execution continues.


4. Best Practices for Exception Handling

To use exceptions effectively, it's important to follow established best practices.

  • When to Use Exceptions: Use exceptions for exceptional errors—those that prevent a function from fulfilling its purpose and cannot be handled locally. Don't use them for predictable control flow (e.g., using throw as a form of goto or an alternative to return).
  • What to Throw: Throw objects of classes that derive from std::exception. This creates a standard hierarchy. The standard library provides several useful types in <stdexcept>, such as std::runtime_error, std::invalid_argument, and std::out_of_range.
  • How to Catch: Catch by const reference (e.g., catch (const std::exception& e)).
    • Catching by reference avoids making a copy of the exception object.
    • It also prevents "slicing," an issue where catching a base class by value strips away information from the derived exception class that was actually thrown.
    • Catching by pointer is discouraged as it introduces ambiguity about memory ownership.
  • Constructors and Destructors:
    • Constructors: It is not only acceptable but often necessary to throw from a constructor if the object cannot be properly constructed. This is the only clean way for a constructor to report a fatal error.
    • Destructors: Never let an exception escape from a destructor. If a destructor is called during stack unwinding for another exception, and it also throws, the program will immediately call std::terminate(). If a destructor must perform an operation that can fail, it should handle that failure internally (e.g., log it) but not propagate an exception.

These rules are essential for writing "exception-safe" code. The C++ FAQ provides more detail on these points.

Exceptions and Error Handling, C++ FAQ

Let's revisit the C++ FAQ to get clear guidance on these best practices.

Please read the following sections: 'What shouldn’t I use exceptions for?': Reinforces that exceptions are for error handling, not control flow. 'Can I throw an exception from a constructor? From a destructor?': This gives the definitive rules for constructors and destructors. 'What should I throw?': Recommends throwing objects that inherit from std::exception. 'What should I catch?': Explains the rationale for catching by reference.


Your Turn: A Practical Exercise

Now it's time to apply what you've learned. Let's create a function that simulates parsing a configuration setting. The function should validate its input and throw an exception if the input is invalid.




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

// This function processes a key-value setting.
// It throws an std::invalid_argument if the key is empty or the value is negative.
void processSetting(const std::string& key, int value) {
    if (key.empty()) {
        // TODO: Throw an std::invalid_argument with a descriptive message.
    }
    if (value < 0) {
        // TODO: Throw an std::invalid_argument with a descriptive message.
    }
    
    std::cout << "Setting processed: " << key << " = " << value << std::endl;
}

int main() {
    // --- Test Case 1: Valid input ---
    try {
        processSetting("speed", 100);
    } catch (const std::invalid_argument& e) {
        std::cerr << "Error processing setting: " << e.what() << std::endl;
    }

    // --- Test Case 2: Invalid key ---
    try {
        processSetting("", 50);
    } catch (const std::invalid_argument& e) {
        std::cerr << "Error processing setting: " << e.what() << std::endl;
    }

    // --- Test Case 3: Invalid value ---
    try {
        processSetting("volume", -5);
    } catch (const std::invalid_argument& e) {
        std::cerr << "Error processing setting: " << e.what() << std::endl;
    }

    return 0;
}

Complete the processSetting function by adding the throw statements. Run the code and verify that the catch blocks in main correctly report the errors for the invalid test cases.

Solution
void processSetting(const std::string& key, int value) {
    if (key.empty()) {
        throw std::invalid_argument("Key cannot be empty.");
    }
    if (value < 0) {
        throw std::invalid_argument("Value cannot be negative. Key: '" + key + "'");
    }
    
    std::cout << "Setting processed: " << key << " = " << value << std::endl;
}

Conclusion

In this lesson, you've learned the fundamentals of C++ exception handling, a critical tool for building robust and reliable applications.

Key Takeaways:

  • Exceptions provide a structured way to handle runtime errors, separating error-handling code from the main program logic.
  • The core mechanism uses try to guard code, throw to signal an error, and catch to handle it.
  • Stack unwinding is the process that ensures local objects are destroyed when an exception is thrown, making exceptions work seamlessly with the RAII principle for resource management.
  • Following best practices is crucial: throw std::exception derivatives, catch by const reference, and never let an exception escape a destructor.

Next Up

Now that you understand the mechanics of C++ exceptions, how do they stack up against the traditional C-style error codes you might encounter in older codebases or low-level APIs? In our next lesson, we will compare exception handling with traditional C-style error codes, discussing the performance implications, use cases, and trade-offs for each approach.