Back to Curriculum

Exception Handling

📚 Lesson 7 of 10 ⏱️ 40 min

Exception Handling

40 min

Exception handling in C++ provides a mechanism for dealing with error conditions that occur during program execution. Unlike error codes that must be checked at every call site, exceptions propagate automatically up the call stack until caught, enabling centralized error handling. This makes error handling more robust and less error-prone than manual error code checking.

C++ exceptions use try-catch blocks where code that might throw exceptions is placed in a try block, and exception handlers are defined in catch blocks. When an exception is thrown, the program searches up the call stack for a matching catch block. If found, execution continues in the catch block; if not, the program terminates. Multiple catch blocks can handle different exception types.

Exceptions are objects that can be of any type, though the standard library provides exception classes in <stdexcept> (like std::runtime_error, std::invalid_argument). Exceptions should be thrown by value and caught by const reference. This pattern ensures proper exception object lifetime and avoids slicing when catching derived exceptions through base class references.

RAII (Resource Acquisition Is Initialization) works seamlessly with exceptions, ensuring resources are properly cleaned up even when exceptions occur. Since destructors are called automatically during stack unwinding (the process of destroying objects as the exception propagates), resources managed by RAII objects are always cleaned up correctly. This eliminates the need for try-finally blocks found in other languages.

Exception safety guarantees specify how code behaves in the presence of exceptions. The basic guarantee ensures no resource leaks and valid state. The strong guarantee ensures operations are atomic (all-or-nothing). The no-throw guarantee ensures functions never throw exceptions. Understanding these guarantees helps design robust code that handles errors correctly.

Custom exception classes can be created by inheriting from std::exception or its derived classes. Custom exceptions should provide meaningful error messages and can include additional context about the error. The what() method (inherited from std::exception) should return a C-string describing the error. Custom exceptions enable domain-specific error handling and better error reporting.

Key Concepts

  • Exceptions provide automatic error propagation up the call stack.
  • try-catch blocks handle exceptions locally.
  • Exceptions are objects, thrown by value and caught by const reference.
  • RAII ensures proper cleanup during exception propagation.
  • Exception safety guarantees specify error handling behavior.

Learning Objectives

Master

  • Using try-catch blocks for exception handling
  • Throwing and catching standard and custom exceptions
  • Understanding exception propagation and stack unwinding
  • Designing exception-safe code with RAII

Develop

  • Understanding error handling strategies
  • Designing robust, exception-safe code
  • Creating meaningful error reporting

Tips

  • Throw by value, catch by const reference: catch (const std::exception& e).
  • Use standard exceptions when appropriate: throw std::invalid_argument("message").
  • Order catch blocks from most specific to most general.
  • Ensure destructors don't throw exceptions (undefined behavior).

Common Pitfalls

  • Catching exceptions by value instead of reference, causing slicing.
  • Not handling exceptions, causing program termination.
  • Throwing exceptions from destructors, causing undefined behavior.
  • Using exceptions for control flow, reducing performance.

Summary

  • Exceptions provide automatic error propagation and handling.
  • try-catch blocks enable local exception handling.
  • RAII ensures proper cleanup during exception propagation.
  • Custom exceptions enable domain-specific error handling.

Exercise

Create custom exceptions and demonstrate exception handling.

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

// Custom exception class
class ValidationError : public std::exception {
private:
    std::string message;
    
public:
    ValidationError(const std::string& msg) : message(msg) {}
    
    const char* what() const noexcept override {
        return message.c_str();
    }
};

class Calculator {
public:
    double divide(double a, double b) {
        if (b == 0) {
            throw std::invalid_argument("Division by zero");
        }
        return a / b;
    }
    
    int validateAge(int age) {
        if (age < 0) {
            throw ValidationError("Age cannot be negative");
        }
        if (age > 150) {
            throw ValidationError("Age seems unrealistic");
        }
        return age;
    }
};

int main() {
    Calculator calc;
    
    // Exception handling examples
    try {
        double result = calc.divide(10, 0);
        std::cout << "Result: " << result << std::endl;
    }
    catch (const std::invalid_argument& e) {
        std::cout << "Error: " << e.what() << std::endl;
    }
    
    try {
        int age = calc.validateAge(-5);
        std::cout << "Valid age: " << age << std::endl;
    }
    catch (const ValidationError& e) {
        std::cout << "Validation error: " << e.what() << std::endl;
    }
    
    return 0;
}

Exercise Tips

  • Create custom exceptions: class MyError : public std::runtime_error {}.
  • Use exception hierarchy: catch specific exceptions before general ones.
  • Test exception safety: ensure resources are cleaned up during exceptions.
  • Use noexcept for functions that shouldn't throw: void func() noexcept {}.

Code Editor

Output