C++ by Example
C++ by Example

Error Handling

2 min read

Error Handling

C++ provides exceptions for signaling and handling errors. Exceptions decouple error detection from error handling and unwind the stack automatically, triggering destructors (and thus RAII cleanup) along the way.

Throwing and catching

#include <iostream>
#include <stdexcept>

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";   // 5
        std::cout << divide(10.0, 0.0) << "\n";   // throws
    }
    catch (const std::invalid_argument& e) {
        std::cout << "Error: " << e.what() << "\n";
    }
}

The standard exception hierarchy

std::exception
├── std::logic_error         — precondition violations, programming errors
│   ├── std::invalid_argument
│   ├── std::out_of_range
│   └── std::domain_error
└── std::runtime_error       — errors detectable only at runtime
    ├── std::range_error
    ├── std::overflow_error
    └── std::underflow_error

Catch the most specific type you can handle. A catch for std::exception handles everything; a catch for std::out_of_range handles only that type:

try { /* ... */ }
catch (const std::out_of_range& e) { /* specific */ }
catch (const std::runtime_error& e) { /* broader */ }
catch (const std::exception& e)     { /* everything */ }
catch (...)                         { /* truly anything, including non-std */ }

Custom exception types

Derive from std::exception or one of its subclasses:

#include <stdexcept>
#include <string>

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

    int code() const { return code_; }
};

void query(const std::string& sql) {
    if (sql.empty()) throw DatabaseError(400, "empty query");
}

int main() {
    try {
        query("");
    }
    catch (const DatabaseError& e) {
        std::cerr << "[" << e.code() << "] " << e.what() << "\n";
    }
}

noexcept

Mark functions that never throw noexcept. The compiler can optimize them better, and the standard library relies on this for move operations:

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

If a noexcept function does throw, std::terminate is called immediately. Destructors are implicitly noexcept — never throw from one.

Re-throwing

try {
    risky_operation();
}
catch (const std::exception& e) {
    log_error(e.what());
    throw;   // re-throw the same exception — preserves the original type
}

Bare throw; re-throws the current exception. throw e; throws a copy (and may slice if e is a base class reference).

Exception safety levels

Functions should aim for one of these guarantees:

Level Guarantee
No-throw Never throws; noexcept
Strong If it throws, the program state is unchanged (commit-or-rollback)
Basic If it throws, the program is in a valid (though possibly changed) state; no leaks
None No guarantee; avoid

RAII makes the basic guarantee easy to achieve: destructors clean up even when exceptions unwind the stack.

C++ by Example
C++ by Example

Learn modern C++ through working code. Each chapter introduces one concept — variables, functions, classes, templates, smart pointers, concurrency — with a clear example, a line-by-line explanation, and notes on how it applies in real programs.

View book →