Skip to content
Home
C++ Exception Handling: Try, Catch, and RAII

C++ Exception Handling: Try, Catch, and RAII

C/C++ C/C++ 8 min read 1552 words Beginner David Chen

Exception handling in C++ is the mechanism for propagating errors up the call stack. When used correctly with RAII (Resource Acquisition Is Initialization), it produces code that is cleaner than error-code alternatives and guarantees resource cleanup even when things go wrong.

Basic Syntax

try {
    // Code that may throw
    int result = divide(10, 0);
    std::cout << result;
--- catch (const std::runtime_error& e) {
    std::cerr << "Runtime error: " << e.what();
--- catch (const std::exception& e) {
    std::cerr << "Exception: " << e.what();
--- catch (...) {
    std::cerr << "Unknown exception";
---

Exceptions are objects of any type, but standard practice is to derive from std::exception. The catch clauses are checked in order, and the first matching handler executes. The ellipsis catch-all (catch (...)) should only be used as a last resort — usually to log and rethrow.

Exception Safety Guarantees

Every C++ function provides one of three exception safety levels:

LevelGuaranteeExample
No-throwNever throwsDestructors, swap, std::move
StrongRolls back on failureTransaction-like operations
BasicNo leaks, valid stateMost operations
No guaranteeAnything can happenLegacy code

No-throw Guarantee

The function will never throw an exception. This is required for destructors, swap, and functions called during stack unwinding.

void swap(Vector& other) noexcept {
    std::swap(size_, other.size_);
    std::swap(data_, other.data_);
---

The noexcept specifier is both documentation and an optimization — the compiler can avoid generating unwinding metadata when it knows a function will not throw.

Strong Guarantee

If an exception is thrown, the program state is unchanged — as if the function was never called.

class Account {
    std::string name_;
    double balance_;
public:
    void deposit(double amount) {
        auto tmp = balance_;  // save state
        try {
            // perform operations that may throw
            balance_ += amount;
        } catch (...) {
            balance_ = tmp;   // restore state
            throw;            // rethrow
        }
    }
---;

The copy-and-swap idiom is the cleanest way to achieve the strong guarantee: operate on a copy, then swap if successful.

Basic Guarantee

The function may throw, but no resources are leaked, invariants are maintained, and the object is in a valid (though possibly unspecified) state.

void Vector::reserve(size_t new_cap) {
    if (new_cap > capacity()) {
        T* new_data = new T[new_cap];      // may throw
        for (size_t i = 0; i < size_; ++i) {
            new_data[i] = std::move(data_[i]);  // may throw
        }
        delete[] data_;
        data_ = new_data;
        capacity_ = new_cap;
    }
---

RAII and Resource Management

RAII ties resource lifetime to object lifetime. When an exception causes stack unwinding, RAII objects automatically release their resources:

void process_file(const std::string& path) {
    std::ifstream file(path);  // RAII — file opens
    if (!file) {
        throw std::runtime_error("Cannot open file");
    }
    std::vector<char> buffer(1024);
    file.read(buffer.data(), buffer.size());
    // file closes automatically when 'file' goes out of scope
    // — whether via normal exit or exception
---

Manual resource management is error-prone:

void leaky_function() {
    int* p = new int(42);
    // If do_work() throws, 'p' leaks
    do_work();
    delete p;
---

RAII containers like std::unique_ptr, std::vector, and std::string eliminate leaks. The rule is: never use raw pointers or arrays for owning resources. Every resource should be owned by a class whose destructor releases it.

noexcept and Exception Specifications

Modern C++ uses noexcept instead of the deprecated throw() dynamic exception specification.

void never_throws() noexcept;                    // Will not throw
void may_throw();                                 // May throw
void conditionally_noexcept() noexcept(condition);// Conditionally noexcept

The noexcept operator checks whether an expression is noexcept:

template<typename T>
void move_if_noexcept(T& src, T& dest) noexcept {
    dest = std::move_if_noexcept(src);
---

std::move_if_noexcept returns an rvalue reference if T has a noexcept move constructor, and an lvalue reference otherwise. This is used in std::vector::reserve — if the move constructor might throw, the vector copies instead, preserving the strong guarantee.

When to Use noexcept

  • Always: destructors, swap, move constructors and assignment
  • Often: getters, simple accessors, functions that only call noexcept functions
  • Sometimes: constructors and factory functions that cannot fail
  • Rarely: complex operations that could reasonably throw

Standard Exception Hierarchy

std::exception
├── std::logic_error
│   ├── std::invalid_argument
│   ├── std::domain_error
│   ├── std::length_error
│   ├── std::out_of_range
│   └── std::future_error
├── std::runtime_error
│   ├── std::range_error
│   ├── std::overflow_error
│   ├── std::underflow_error
│   ├── std::regex_error
│   ├── std::system_error
│   ├── std::ios_base::failure
│   └── std::filesystem::filesystem_error
└── std::bad_alloc
    └── std::bad_array_new_length

Derive custom exceptions from std::runtime_error or std::logic_error:

class InsufficientFundsError : public std::runtime_error {
public:
    explicit InsufficientFundsError(double balance, double requested)
        : std::runtime_error(
            "Insufficient funds: have " + std::to_string(balance)
            + ", need " + std::to_string(requested)),
          balance_(balance), requested_(requested) {}
    double balance() const { return balance_; }
    double requested() const { return requested_; }
private:
    double balance_;
    double requested_;
---;

Performance Considerations

Exception handling has a cost model:

  • Zero-cost when no exception is thrown — the generated code has no overhead for the happy path (using a separate unwind table)
  • Costly when an exception is thrown — stack unwinding involves searching the call stack, calling destructors, and matching catch handlers
  • Code size increase — the unwind tables and catch handler metadata add to binary size

This makes exceptions ideal for truly exceptional circumstances (failure to open a file, out of memory) and unsuitable for control flow (end-of-file, not-found lookups). For expected failure modes, use std::optional, std::expected (C++23), or error codes.

Best Practices

  1. Throw by value, catch by reference — prevents slicing and avoids copying
  2. Use RAII for all resources — never write raw new/delete
  3. Mark destructors and swap noexcept — containers rely on this for correctness
  4. Prefer std::optional for expected failures — not-found and end-of-file are not exceptional
  5. Use noexcept for move operations — enables performance optimizations in containers
  6. Never throw from a destructor — causes std::terminate during stack unwinding
  7. Keep exception classes lightweight — they are copied during throw
  8. Log the exception at the boundary — catch at the top level of a thread or request handler

The goal is not to eliminate exceptions but to use them where they provide real value: separating error handling from normal control flow while guaranteeing resource cleanup.

Stack Unwinding and RAII

When an exception is thrown, C++ performs stack unwinding — destructors are called for all local objects between the throw site and the catch block, in reverse order of construction. This is why RAII is critical in exception-prone code: if a mutex is locked via std::lock_guard, the destructor releases it during unwinding, preventing deadlocks:

void transfer_money(Account &from, Account &to, double amount) {
    std::lock_guard<std::mutex> lock(from.mtx);  // Locks first mutex
    std::lock_guard<std::mutex> lock2(to.mtx);   // Locks second mutex
    // If deduction throws, both mutexes are released automatically
    from.balance -= amount;
    to.balance += amount;
---

Without RAII, an exception between locking the first and second mutex would leave the first mutex locked permanently. This is why C++ experts emphasize RAII for all resource management — files, memory, locks, and database connections.

noexcept Specifier

The noexcept keyword tells the compiler that a function will not throw. This enables optimizations — the compiler can skip the exception-handling bookkeeping for that function. It also enables move semantics in the standard library: std::vector uses move instead of copy when the move constructor is noexcept.

class MyData {
public:
    MyData(MyData &&other) noexcept
        : data(std::exchange(other.data, nullptr)) {}
    // noexcept enables vector to use move instead of copy during reallocation
---;

Mark functions noexcept when you are certain they will not throw. If a noexcept function does throw, std::terminate() is called — so be conservative.

Exception Safety Guarantees

Functions provide three levels of exception safety:

  1. Basic guarantee — If an exception occurs, no resources leak and the object remains in a valid (but unspecified) state.
  2. Strong guarantee — If an exception occurs, the operation is rolled back to its original state (commit-or-rollback semantics).
  3. No-throw guarantee — The operation will never throw. Destructors, swap, and move operations should provide this.

When to Use Exceptions vs Error Codes

Use exceptions for errors that cannot be handled locally — out of memory, file not found, network failure. Use error codes for expected failure modes where the caller should handle the result immediately — end of file, invalid input, timeout. Exceptions should be exceptional: they represent failure conditions, not control flow.

FAQ

What is the difference between malloc and new in C++? malloc allocates raw memory without calling constructors; new allocates memory and calls the constructor. In C++, prefer new for objects. free vs delete follows the same pattern — delete calls the destructor.

How do I prevent memory leaks in C/C++? Use RAII (Resource Acquisition Is Initialization) in C++ — smart pointers like std::unique_ptr and std::shared_ptr automatically free memory. In C, always pair every malloc with a free and use tools like Valgrind or AddressSanitizer to detect leaks.

What is undefined behavior in C/C++? Undefined behavior occurs when code performs operations that the language standard does not define — dereferencing a null pointer, buffer overflow, signed integer overflow. The compiler may generate any code, including unexpected results or crashes.

Should I learn C or C++ first? Learn C first if you want to understand low-level memory and system programming. Learn C++ first if you want object-oriented features and the STL. Both are valuable; C++ builds on C concepts.

What is the difference between a header file and a source file? Header files (.h) declare interfaces — function prototypes, class definitions, macros. Source files (.c or .cpp) implement the declarations. Headers are #included; source files are compiled separately and linked.

For a comprehensive overview, read our article on C File Io Guide.

For a comprehensive overview, read our article on C Memory Management.

Section: C/C++ 1552 words 8 min read Beginner 1251 articles in section Report inaccuracy Back to top
DC
David Chen Technology Editor

Technology Editor at ExcellentWiki covering programming, engineering, and consumer technology topics.

View author profile →