CppException Handling

Exception Handling

Not every error can be caught at compile time. A file might not exist, a network request might fail, or a caller might pass an invalid argument. C++ handles these situations with exceptions — a mechanism for signaling an error from deep inside a call stack and letting some outer piece of code decide how to respond, without every function in between needing to check for it manually.
try, throw, and catch
Exception handling revolves around three keywords. Code that might fail goes inside a try block. If something goes wrong, that code raises (or "throws") an exception object with throw. Execution immediately leaves the try block and jumps to a matching catch block, which handles the error.

basic-exception.cpp

CPP
#include <iostream>
#include <stdexcept>

double divide(double a, double b) {
  if (b == 0.0) {
    throw std::runtime_error("division by zero");
  }
  return a / b;
}

int main() {
  try {
    std::cout << divide(10, 2) << "\n"; // 5
    std::cout << divide(5, 0) << "\n";  // throws before this prints
  } catch (const std::runtime_error& e) {
    std::cout << "Error: " << e.what() << "\n";
  }

  std::cout << "Program keeps running.\n";
  return 0;
}
Once divide(5, 0) throws, the rest of the try block is skipped and control jumps straight to the matching catch block. The program does not crash — it recovers and keeps going.
Standard exception types
The <stdexcept> header provides a family of ready-made exception classes for common failure categories, so you rarely need to invent your own error type for ordinary cases.

Type

Typical use

std::runtime_error

A general error only detectable at runtime (I/O failure, out-of-range computation).

std::invalid_argument

A function received an argument that makes no sense (e.g. a negative size).

std::out_of_range

An index or key was outside the valid bounds of a container (e.g. vector::at).

std::logic_error

An error that in principle could have been avoided by the caller checking first.

Always catch by reference
Every standard exception derives from std::exception, which exposes a virtual what() method returning a human-readable description. That inheritance is exactly why catch parameters matter.
Catching by value slices the exception
If you write catch (std::exception e), a thrown std::out_of_range object gets copied into a plain std::exception — the derived-class part is "sliced" off, and e.what() loses the specific message. Always catch by const reference: catch (const std::exception& e). It avoids the slicing, avoids an unnecessary copy, and still works polymorphically.
Multiple catch blocks and catch-all
You can chain several catch blocks to handle different exception types differently. They are tried in order, so put more specific types before more general ones. A final catch (...) catches literally anything, including exception types you didn't anticipate — useful as a last-resort safety net.

multiple-catch.cpp

CPP
#include <iostream>
#include <stdexcept>
#include <vector>

int main() {
  std::vector<int> values = {1, 2, 3};

  try {
    std::cout << values.at(10) << "\n"; // throws std::out_of_range
  } catch (const std::out_of_range& e) {
    std::cout << "Index problem: " << e.what() << "\n";
  } catch (const std::exception& e) {
    std::cout << "Some other standard exception: " << e.what() << "\n";
  } catch (...) {
    std::cout << "Something totally unexpected was thrown.\n";
  }

  return 0;
}
Writing a custom exception
For domain-specific errors, define your own exception class by inheriting from std::exception (or a more specific standard type) and overriding what(). This lets callers catch your exception by name, or fall back to catching it generically as a std::exception.

custom-exception.cpp

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

class InsufficientFundsError : public std::exception {
public:
  explicit InsufficientFundsError(double shortfall) : shortfall_(shortfall) {
    message_ = "insufficient funds, short by " + std::to_string(shortfall_);
  }

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

private:
  double shortfall_;
  std::string message_;
};

void withdraw(double balance, double amount) {
  if (amount > balance) {
    throw InsufficientFundsError(amount - balance);
  }
}

int main() {
  try {
    withdraw(100.0, 150.0);
  } catch (const InsufficientFundsError& e) {
    std::cout << "Withdrawal failed: " << e.what() << "\n";
  }
  return 0;
}
Note the noexcept on what() — it matches the base class signature and promises the method itself never throws, which is important since it's typically called while already handling another exception.
RAII and exception safety
An exception can unwind the call stack from deep inside nested function calls, skipping over ordinary cleanup code on its way out. If a function manually called delete, closed a file, or released a lock at the bottom of its body, an exception thrown earlier would skip right past that cleanup — leaking memory or leaving a lock held forever.
This is why RAII matters so much in C++
RAII (Resource Acquisition Is Initialization) ties a resource's lifetime to an object's lifetime. When the stack unwinds during exception propagation, C++ guarantees that destructors for every local object in scope still run — automatically, in reverse order. Smart pointers, std::lock_guard, and file streams all rely on this: cleanup happens no matter how the function exits, including via an exception.
  • Prefer throwing exception objects by value and catching by const reference.

  • Never let a destructor throw — an exception escaping a destructor during stack unwinding calls std::terminate().

  • Use RAII wrappers (smart pointers, lock guards, fstream objects) instead of manual acquire/release pairs so cleanup survives exceptions.

  • Catch exceptions only where you can meaningfully handle them — letting them propagate is often the right choice.