Back to Articles

Exception Handling in C++

SkillStream Editorial
August 27, 2026

A practical guide to exception handling in C++ — how try, catch, and throw work together, standard exception types, writing custom exceptions, and how exceptions interact with RAII.

Exception Handling in C++

ot every error can be checked in advance — a file might not exist, a network call might fail, a user might pass in invalid input. C++'s exception handling mechanism exists to deal with these situations without littering every function with manual error-code checks.

The Three Keywords

  • try — wraps code that might fail
  • throw — signals that something has gone wrong, and hands off an exception object describing it
  • catch — receives that exception object and decides how to respond
cpp
#include <iostream> #include <stdexcept> double divide(double a, double b) { if (b == 0) { throw std::runtime_error("Division by zero"); } return a / b; } int main() { try { double result = divide(10, 0); std::cout << result; } catch (const std::runtime_error& e) { std::cout << "Error: " << e.what() << std::endl; } }

When divide() throws, execution immediately stops at that point and jumps to the nearest matching catch block — skipping everything else in try in between.

Standard Exception Types

C++'s <stdexcept> header provides a hierarchy of ready-made exception types instead of forcing every project to invent its own:

  • std::runtime_error — errors detectable only at runtime
  • std::logic_error — errors from flawed program logic
  • std::out_of_range — accessing an invalid index or range
  • std::invalid_argument — a function received an unacceptable argument
  • std::bad_alloc — memory allocation failed All of these inherit from std::exception, which is why catching const std::exception& will catch any of them.

Catching Multiple Exception Types

cpp
try { riskyOperation(); } catch (const std::out_of_range& e) { std::cout << "Out of range: " << e.what(); } catch (const std::exception& e) { std::cout << "General error: " << e.what(); }

Order matters here — more specific exception types should be caught before more general ones, since the first matching catch block wins.

Writing a Custom Exception

For domain-specific errors, define your own exception class by inheriting from std::exception:

cpp
class InsufficientFundsError : public std::exception { public: const char* what() const noexcept override { return "Insufficient funds for this transaction"; } }; void withdraw(double balance, double amount) { if (amount > balance) { throw InsufficientFundsError(); } }

This lets calling code catch and respond to your specific error type, rather than parsing a generic message string.

Exceptions and RAII: Why They're Designed Together

One of the strongest reasons C++ exceptions work well is their interaction with RAII (see the constructors/destructors post for the full pattern): when an exception is thrown, C++ guarantees that every local object already constructed on the way to the throw gets its destructor called during "stack unwinding" — so files get closed, locks get released, and memory gets freed automatically, even though the function never reached its normal end.

cpp
void process() { std::lock_guard<std::mutex> lock(mtx); // acquires a lock riskyOperation(); // if this throws... } // ...the lock is still released automatically during unwinding

When Not to Use Exceptions

Exceptions carry real runtime cost and are best reserved for genuinely exceptional situations — not routine control flow. Using them for predictable, frequent outcomes (like "user typed an invalid menu option") is generally considered worse practice than a simple return value or error code, both for performance and for readability.

The Takeaway

try, throw, and catch give C++ a clean way to separate "the code that does the work" from "the code that decides what to do when something goes wrong" — and because they're built to cooperate with RAII, they let you handle failures without leaking resources along the way.