Skip to content
C++
Language
since C++11
Basic

Master Exception Handling to Write Robust, Error-Resilient C++

Learn to throw, catch, and design exception hierarchies so your C++ programs handle errors safely without leaking resources.

By the end of this page, you will know how to throw and catch exceptions, build a custom exception hierarchy, use noexcept correctly, and avoid the three most common mistakes that turn exception handling into a source of bugs rather than a fix for them.

What and Why

Imagine a function that opens a file. It can fail β€” the file might not exist, permissions might be denied, the disk might be full. One approach is to return an error code:

cpp
int open_file(const char* path);  // returns 0 on success, -1 on failure

This works, but callers can silently ignore the return value, and threading error information through many layers of call stack means every intermediate function must check and forward the code β€” even if it has nothing useful to add.

Exceptions solve this differently: when something goes wrong, you throw an object describing the problem. That object automatically travels up the call stack until some caller catches it. Any frame that doesn't catch it just passes it upward β€” no boilerplate required. If nothing catches it, the program terminates with a diagnostic.

The key insight is that exceptions are for exceptional situations β€” events that the immediate caller cannot reasonably handle and that represent a deviation from the function's normal contract. Routine conditions (user typed bad input, a file wasn't found in a search path) are usually better served by return values or std::optional. Exceptions shine when something goes structurally wrong.

Step by Step

Throwing and catching

cpp
#include <stdexcept>
#include <iostream>

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

int main() {
    try {
        std::cout << divide(10.0, 2.0) << '\n';  // prints 5
        std::cout << divide(10.0, 0.0) << '\n';  // throws
    } catch (const std::invalid_argument& e) {
        std::cerr << "Error: " << e.what() << '\n';
    }
}

The try block wraps code that might throw. The catch block names the exception type it handles. Execution jumps directly from the throw to the matching catch β€” everything between is unwound.

Catch by const reference, not by value. Catching by value copies the exception object and slices derived types down to the base.

The standard exception hierarchy

The standard library provides a ready-made hierarchy rooted at std::exception:

cpp
std::exception
β”œβ”€β”€ std::logic_error       (violated preconditions, programmer errors)
β”‚   β”œβ”€β”€ std::invalid_argument
β”‚   β”œβ”€β”€ std::out_of_range
β”‚   └── std::length_error
└── std::runtime_error     (external conditions the program can't control)
    β”œβ”€β”€ std::overflow_error
    β”œβ”€β”€ std::underflow_error
    └── std::system_error  (OS/IO errors, carries an error_code)

You can catch at any level:

cpp
try {
    risky_operation();
} catch (const std::logic_error& e) {
    // programmer bug β€” log and abort
} catch (const std::runtime_error& e) {
    // environmental failure β€” retry or degrade gracefully
} catch (const std::exception& e) {
    // catch-all for any standard exception
} catch (...) {
    // catch absolutely everything, including non-standard throws
}

catch clauses are tested top to bottom. Put more specific types first.

Custom exception types

cpp
#include <stdexcept>
#include <string>

class DatabaseError : public std::runtime_error {
public:
    explicit DatabaseError(const std::string& msg, int code)
        : std::runtime_error(msg), error_code_(code) {}

    int error_code() const noexcept { return error_code_; }

private:
    int error_code_;
};

void connect(const std::string& dsn) {
    throw DatabaseError("connection refused", 1049);
}

int main() {
    try {
        connect("mysql://localhost/mydb");
    } catch (const DatabaseError& e) {
        // catches DatabaseError specifically
        std::cerr << "[" << e.error_code() << "] " << e.what() << '\n';
    } catch (const std::runtime_error& e) {
        // would also catch DatabaseError if the above weren't there
        std::cerr << "Runtime error: " << e.what() << '\n';
    }
}

Inheriting from std::runtime_error or std::logic_error means callers who only know about the base types can still catch your exceptions β€” crucial for library boundaries.

noexcept β€” promising you won't throw

Mark a function noexcept when it genuinely cannot throw:

cpp
int add(int a, int b) noexcept { return a + b; }

If a noexcept function does throw (perhaps via a called function), std::terminate is called immediately. That might sound harsh, but it's the right tradeoff: move constructors and swap functions should be noexcept so the standard library can use them in strong-exception-safe algorithms. A throwing move constructor forces std::vector to copy instead of move during reallocation.

cpp
class Buffer {
public:
    Buffer(Buffer&& other) noexcept;   // lets vector move efficiently
    Buffer& operator=(Buffer&& other) noexcept;
    ~Buffer() noexcept;                // destructors are implicitly noexcept
};

Common Patterns

RAII makes exceptions safe automatically

The single most important technique for exception safety is RAII (Resource Acquisition Is Initialization). When an exception unwinds the stack, all local objects with automatic storage duration are destroyed β€” their destructors run. If resources are tied to object lifetime, they cannot leak.

cpp
#include <fstream>
#include <string>

std::string read_file(const std::string& path) {
    std::ifstream file(path);   // opens on construction
    if (!file.is_open())
        throw std::runtime_error("cannot open: " + path);

    std::string content((std::istreambuf_iterator<char>(file)),
                         std::istreambuf_iterator<char>());
    return content;
    // file closes here β€” even if an exception was thrown above
}

No try/finally needed. The destructor handles cleanup unconditionally.

Re-throwing inside a catch block

Sometimes you want to log and then let the exception continue propagating:

cpp
void process() {
    try {
        load_data();
    } catch (const std::exception& e) {
        log_error(e.what());
        throw;   // re-throw the *original* exception, not a copy
    }
}

Use bare throw; β€” not throw e;. The latter creates a new copy of e as a std::exception, losing any derived-type information.

Translating exceptions at layer boundaries

At the boundary between a library and application code, translate low-level exceptions into domain-specific ones:

cpp
UserRecord load_user(int id) {
    try {
        return db_.query("SELECT * FROM users WHERE id = ?", id);
    } catch (const DatabaseError& e) {
        throw UserNotFoundError(id, e.what());
    }
}

This keeps callers insulated from implementation details and keeps exception hierarchies meaningful at each abstraction level.

What Can Go Wrong

Catching by value (slicing)

cpp
// Wrong
catch (std::exception e) { ... }

// Right
catch (const std::exception& e) { ... }

Catching by value copies the exception and slices off any derived members. e.what() may return a meaningless base-class message.

Throwing from a destructor

cpp
class Bad {
public:
    ~Bad() {
        throw std::runtime_error("oops");  // undefined behavior if already unwinding
    }
};

If a destructor throws while the stack is already being unwound due to another exception, std::terminate is called. Destructors must be noexcept. Handle errors in destructors by catching internally and logging, not propagating.

Swallowing exceptions silently

cpp
// Wrong β€” hides bugs
try {
    do_something();
} catch (...) {}

// Right β€” at minimum, log before swallowing
try {
    do_something();
} catch (const std::exception& e) {
    log_error(e.what());
} catch (...) {
    log_error("unknown exception");
}

An empty catch block that discards every exception makes bugs impossible to diagnose. If you genuinely need to swallow, at minimum log the exception type and message.

Quick Reference

Keyword / ToolPurpose
throw exprThrow an exception; begins stack unwinding
try { } catch (T& e) { }Handle exceptions of type T or its bases
throw;Re-throw the current exception (inside a catch)
catch (...)Catch anything, including non-std::exception types
noexceptPromise a function won't throw; enables optimizations
e.what()Retrieve the message from any std::exception
std::exception_ptrStore and transport an exception across threads
std::current_exception()Capture the active exception as exception_ptr
std::rethrow_exception(p)Re-throw a captured exception_ptr

Exception safety levels (strongest to weakest):

  • No-throw β€” guaranteed not to throw (noexcept)
  • Strong β€” operation succeeds or leaves state unchanged
  • Basic β€” state remains valid but unspecified on failure
  • No guarantee β€” anything could happen (avoid)

What's Next