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 ExcellentWiki Editorial Team

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 756 articles in section Report inaccuracy Back to top