Skip to content

C++ Exception Handling

An exception is an unexpected event or error condition that occurs during program execution, disrupting the normal flow of instructions. Examples include attempting to divide by zero, failing to allocate memory, or encountering invalid input.

C++ provides a structured mechanism for handling such exceptional circumstances using three keywords: try, catch, and throw.

  • try: A try block encloses a section of code where exceptions might occur. It signifies that you are prepared to handle potential exceptions raised within this block.
  • throw: When an error condition is detected, an exception is thrown using the throw keyword followed by an expression. The type of the expression determines the type of the exception thrown.
  • catch: A catch block immediately follows a try block (or another catch block). It acts as an exception handler. It specifies the type of exception it can catch and contains the code to handle that exception.

The basic syntax looks like this:

try {
// Code that might throw an exception (protected code)
// ...
} catch (const ExceptionType1& e1) { // Catch specific exception type by const reference
// Handle ExceptionType1
// Access exception details via e1 (e.g., e1.what() for std::exception derived types)
} catch (const ExceptionType2& e2) {
// Handle ExceptionType2
// ...
} catch (...) { // Catch-all handler (use sparingly)
// Handle any other unexpected exception type
}

Code inside the try block is executed normally. If an exception is thrown within the try block (or in a function called from within it), the program immediately jumps to find a matching catch block. The first catch block whose type matches the thrown exception (or is a base class of it) is executed. If no matching catch block is found, the program terminates (typically by calling std::terminate).

You can throw an exception from anywhere using the throw statement. The operand can be of any type, but it’s strongly recommended to throw objects of classes derived from std::exception.

Example: Throwing an exception for division by zero.

#include <stdexcept> // Required for standard exception classes like std::runtime_error
#include <string>
double safe_division(int a, int b) {
if (b == 0) {
// Throw an object of a standard exception type
throw std::runtime_error("Division by zero condition!");
}
// Note: integer division truncates. Cast for floating-point division.
return static_cast<double>(a) / b;
}

The catch block specifies the type of exception it can handle. It’s best practice to catch exceptions by constant reference (const ExceptionType& e). This avoids unnecessary copying and prevents slicing if catching polymorphic exception types.

try {
// ... code that might call safe_division ...
double result = safe_division(10, 0);
// ... this part won't be reached if exception is thrown ...
} catch (const std::runtime_error& e) {
// Handle the specific runtime error
std::cerr << "Error caught: " << e.what() << std::endl;
// e.what() returns the message passed during construction ("Division by zero condition!")
}

You can use multiple catch blocks to handle different exception types. They are checked in order. A catch-all handler catch (...) will catch any type of exception, but it provides no information about the exception. Use it cautiously, often for cleanup before re-throwing.

Example demonstrating try/catch:

#include <iostream>
#include <stdexcept>
#include <string>
// Assume safe_division function from above is defined here
double safe_division(int a, int b) {
if (b == 0) {
throw std::runtime_error("Division by zero condition!");
}
return static_cast<double>(a) / b;
}
int main() {
int x = 50;
int y = 0;
double z = 0;
try {
std::cout << "Attempting division..." << std::endl;
z = safe_division(x, y);
std::cout << "Result: " << z << std::endl; // This line won't execute
} catch (const std::runtime_error& e) {
// Catch the specific exception type by const reference
std::cerr << "Caught a runtime error: " << e.what() << std::endl;
} catch (const std::exception& e) {
// Catch any other standard exception (if safe_division threw a different one)
std::cerr << "Caught a standard exception: " << e.what() << std::endl;
} catch (...) {
std::cerr << "Caught an unknown exception type." << std::endl;
}
std::cout << "Program continues after try-catch block." << std::endl;
return 0;
}

When compiled and run, this produces:

Attempting division...
Caught a runtime error: Division by zero condition!
Program continues after try-catch block.

The C++ Standard Library provides a hierarchy of standard exception classes defined in the <stdexcept> header (and base class std::exception in <exception>). Throwing these standard types (or types derived from them) is recommended.

The base class for all standard exceptions is std::exception. Key derived classes include:

  • std::logic_error: Represents errors in the internal logic of the program (preconditions violated, etc.). Often theoretically detectable before runtime.
  • std::domain_error: Domain error for mathematical functions.
  • std::invalid_argument: Invalid argument passed to a function.
  • std::length_error: Attempt to create an object larger than the maximum supported size.
  • std::out_of_range: Argument value is not in the expected range (e.g., accessing vector element outside bounds via .at()).
  • std::runtime_error: Represents errors that can likely only be detected at runtime.
  • std::overflow_error: Arithmetic overflow occurred.
  • std::underflow_error: Arithmetic underflow occurred.
  • std::range_error: Result of a computation cannot be represented.
  • std::system_error (C++11): Represents low-level operating system errors.
  • std::bad_alloc: Thrown by new when memory allocation fails.
  • std::bad_cast: Thrown by dynamic_cast when casting a reference fails.
  • std::bad_typeid: Thrown by typeid on a null pointer.
  • std::bad_exception: Used with unexpected exception handling mechanisms.

All classes derived from std::exception have a virtual what() member function that returns a C-style string (const char*) describing the exception.

You can define your own exception classes for application-specific errors. It’s best practice to derive custom exception classes from std::exception or one of its standard derivatives (like std::runtime_error or std::logic_error).

#include <iostream>
#include <exception>
#include <string>
// Define a custom exception derived from std::runtime_error
class MyCustomException : public std::runtime_error {
public:
// Constructor accepts a message string
explicit MyCustomException(const std::string& message)
: std::runtime_error(message) {} // Pass message to the base class constructor
// Optional: Add custom members or methods if needed
// const char* what() const noexcept override; // Already provided by std::runtime_error
};
int main() {
try {
// Simulate an error condition
throw MyCustomException("Something specific went wrong in my application.");
} catch (const MyCustomException& e) {
std::cerr << "Caught MyCustomException: " << e.what() << std::endl;
} catch (const std::exception& e) {
// This would catch other standard exceptions if they occurred
std::cerr << "Caught standard exception: " << e.what() << std::endl;
}
return 0;
}

This produces:

Caught MyCustomException: Something specific went wrong in my application.

Writing exception-safe code is crucial. This means ensuring your program remains in a valid state even if exceptions are thrown. The primary C++ technique for achieving this is RAII (Resource Acquisition Is Initialization). RAII uses object lifetimes (managed by constructors and destructors) to automatically manage resources like memory, file handles, locks, etc. When an exception is thrown, destructors of objects on the stack are automatically called, ensuring cleanup happens correctly. Smart pointers (std::unique_ptr, std::shared_ptr) and standard library containers heavily rely on RAII.

You can declare a function with noexcept to specify that it does not throw any exceptions. This can allow for compiler optimizations. If a noexcept function does throw an exception, std::terminate is called. Destructors should generally be noexcept.

void function_that_never_throws() noexcept { /* ... */ }