C++: Exception Handling

Learn how to detect, signal, and recover from runtime errors using try, catch, and throw, including custom exceptions and stack unwinding.

What are Exceptions?

An exception is an event that disrupts the normal flow of a program at runtime, a file that cannot be opened, a division by zero, an index out of range. Rather than checking every function return value for an error code, C++ lets you throw an exception at the point of failure and catch it in a separate error-handling block, keeping the happy path and the error path cleanly separated.

Exceptions vs Error Codes

Error codes must be checked after every call and are easy to ignore silently. Exceptions cannot be ignored, if no catch block handles them, the program terminates.

  • Error codes: every caller must check the return value and propagate it up manually
  • Exceptions: automatically propagate up the call stack until a matching catch is found
  • Business logic stays in try blocks; error handling stays in catch blocks
  • C++ exceptions carry type information, so different errors can be handled differently

try and catch

Wrap code that might fail inside a try block. Immediately after, write one or more catch blocks that specify what type of exception they handle. When an exception is thrown, C++ skips the rest of the try block and searches the catch blocks in order for a match.

try-catch structure

The try block contains the risky code. The catch block contains the recovery code. Code after the catch runs normally whether or not an exception occurred.

  • Syntax: try { risky code; } catch (ExceptionType& e) { handle; }
  • Catch by const reference to avoid copying and to preserve the exception's type
  • Multiple catch blocks can follow a single try
  • Code after the entire try-catch block runs normally in both the success and caught-exception cases

Basic try-catch: Division by Zero

C++

throw stops execution inside divide(). catch intercepts it. The line after the throw is never reached.

The throw Statement

throw can emit any type: a string, an integer, or most usefully a standard or custom exception object. When throw executes, C++ immediately begins unwinding the call stack, searching upward through callers for a matching catch block.

throw

Throw exception objects, not raw strings or integers. Exception objects carry structured information and participate in the inheritance hierarchy, enabling catch-by-base-class.

  • throw value;, throws a copy of value
  • throw ExceptionType(message);, constructs and throws in one step
  • throw; (inside a catch), re-throws the current exception unchanged
  • Throwing from a destructor during stack unwinding calls std::terminate, avoid it

Throwing Different Exception Types

C++

invalid_argument and out_of_range both inherit from exception, so a single catch(const exception&) handles both.

Multiple catch Blocks

A single try block can be followed by multiple catch blocks, each handling a different exception type. C++ checks them in order from top to bottom and executes the first one that matches. Place more specific (derived) exception types before more general (base) ones, a base class catch would swallow all derived types before they get a chance to match.

Ordering catch blocks

Always put derived exception types before their base types. A catch(exception&) placed first will match every standard exception, preventing all the specific handlers below it from ever running.

  • C++ picks the first matching catch, not the best match
  • Specific types first: invalid_argument before logic_error before exception
  • catch(...) must always be last, it matches everything
  • If no catch matches anywhere in the call stack, std::terminate is called

Multiple catch Blocks

C++

Each exception type has its own handler. The most specific types appear first.

Catch-All: catch(...)

The ellipsis catch catch (...) matches any thrown value regardless of type, even a raw integer or string. It is used as a last-resort safety net to prevent an unhandled exception from crashing the program. Since the caught value is inaccessible, it is mainly used for logging or cleanup before re-throwing.

catch (...)

Place catch(...) last. It is a safety net, not a substitute for typed catch blocks. You cannot inspect the caught exception inside it.

  • Catches any thrown value: objects, integers, strings, pointers
  • The caught value is not accessible inside the block
  • Common use: log an unknown error, then re-throw with throw; to let a higher-level handler deal with it
  • Must always be the last catch block if present

catch(...) as Safety Net

C++

The typed catch handles runtime_error. catch(...) catches the int and C-string throws that have no specific handler.

Standard Exception Classes

C++ ships with a hierarchy of exception classes in <stdexcept> and <exception>. All inherit from std::exception, so a single catch(const exception&) can handle any of them. Use the most specific type that accurately describes the error.

Standard Exception Hierarchy

All standard exceptions provide a what() method that returns a C-string description. Inherit from the most appropriate base when creating custom exceptions.

  • logic_error: errors detectable before runtime (bad_argument, out_of_range)
  • runtime_error: errors only detectable at runtime (overflow_error, underflow_error)
  • invalid_argument: an argument's value is not acceptable
  • out_of_range: an index or value is outside a valid range
  • bad_alloc: thrown by new when memory allocation fails
  • bad_cast: thrown by dynamic_cast on an invalid reference cast
ClassHeaderWhen to use
exception<exception>Base for all standard exceptions, catch-all base type
logic_error<stdexcept>Programming mistakes detectable before runtime
runtime_error<stdexcept>Errors only known at runtime
invalid_argument<stdexcept>Bad argument value passed to a function
out_of_range<stdexcept>Index or value outside the valid range
overflow_error<stdexcept>Arithmetic overflow
bad_alloc<new>new fails to allocate memory
bad_cast<typeinfo>dynamic_cast fails on a reference

Custom Exception Classes

You can create your own exception types by inheriting from std::exception or one of its subclasses and overriding what(). Custom exceptions carry domain-specific information, such as an account number or an invalid value, that generic exceptions cannot express.

Custom Exception

Inherit from runtime_error or logic_error rather than directly from exception. You get what() support for free by passing the message to the base constructor.

  • Inherit from std::runtime_error (runtime problems) or std::logic_error (programming mistakes)
  • Pass the message string to the base constructor: runtime_error(msg)
  • Add extra fields for domain-specific data: account number, invalid value
  • Catch by your custom type for precise handling; catch by base class for fallback

Custom Exception: InsufficientFundsException

C++

The custom exception carries the shortfall amount. what() works via the base constructor; shortfall() is domain-specific.

Stack Unwinding

When an exception is thrown, C++ unwinds the call stack: it exits each active function frame in reverse order, calling the destructor of every local object along the way. This guarantees that resources held by stack objects, such as open files and allocated memory managed by RAII wrappers, are released even when an exception bypasses the normal return path.

Stack Unwinding

Stack unwinding is why RAII works: destructors always run, even during exception propagation, so resources are always cleaned up.

  • Local objects are destroyed in reverse order of construction as each frame is exited
  • Destructors must not throw, an exception thrown during unwinding calls std::terminate
  • RAII classes (file streams, smart pointers) rely on this guarantee for safe resource release
  • The finally block in Java/Python is replaced by destructors in C++

Stack Unwinding in Action

C++

Both Guard destructors run automatically as the stack unwinds, inner-Guard first, then outer-Guard.

Re-throwing Exceptions

Inside a catch block, a bare throw; with no operand re-throws the currently active exception unchanged. This is used to perform local cleanup, logging, releasing a lock, and then pass the same exception up to a higher-level handler that knows what to do with it.

Re-throw with throw;

Use throw; not throw e;, rethrowing by name creates a copy and may slice a derived exception down to the base type.

  • throw; preserves the original exception type and object
  • throw e; copies e and may slice a derived object to the catch type
  • Common pattern: catch, log or partially handle, then throw; to propagate
  • Can only be used inside a catch block, using it elsewhere calls std::terminate

Re-throwing an Exception

C++

run() logs the error locally, then throw; passes the original exception up to main's catch block.

Knowledge Check

1. What keyword is used to signal that an error has occurred in C++?

2. What happens to code in a try block after an exception is thrown?

3. Which catch block catches every exception regardless of type?

4. What is the correct way to catch a standard exception by reference?

5. How do you create a custom exception class in C++?

6. What is stack unwinding?

7. Which standard exception class is best for errors detectable at runtime, such as division by zero?

8. How do you re-throw the currently caught exception?

9. In a chain of multiple catch blocks, how does C++ select which one to execute?