Exceptions: throw, catch, noexcept
Exceptions are C++'s mechanism for signaling and handling errors that are exceptional — failures that a function cannot reasonably handle itself and must propagate up the call stack to a caller that can. When an exception is thrown, the stack unwinds: destructors for all local objects run automatically (RAII is preserved), and control jumps to the nearest matching catch block. Understanding how to throw and catch correctly — and when not to use exceptions at all — is essential for writing robust C++.
Throwing exceptions
You can throw any copyable value, but the convention is to throw objects derived from std::exception. The standard library provides a rich hierarchy of exception types in <stdexcept>. When an exception is thrown, stack unwinding begins: every local variable in every function on the call stack between the throw and the catch has its destructor called.
#include <stdexcept>
double divide(double a, double b) {
if (b == 0.0)
throw std::invalid_argument("division by zero");
return a / b;
}
// Standard exception hierarchy (all in <stdexcept>):
// std::exception
// std::logic_error — programming errors, detectable before runtime
// std::invalid_argument
// std::out_of_range
// std::length_error
// std::runtime_error — errors detectable only at runtime
// std::overflow_error
// std::underflow_error
// std::range_errorCatching exceptions
A try block surrounds code that might throw. One or more catch blocks follow, each matching a specific exception type. Catch handlers are tried in order — the first match wins. Catch by const& to avoid slicing and unnecessary copies. A final catch(...) catches any exception type.
try {
double result = divide(10.0, 0.0);
std::cout << result;
}
catch (const std::invalid_argument& e) {
std::cerr << "Invalid argument: " << e.what() << "\n";
}
catch (const std::runtime_error& e) {
std::cerr << "Runtime error: " << e.what() << "\n";
}
catch (const std::exception& e) {
std::cerr << "Unknown std exception: " << e.what() << "\n";
}
catch (...) {
std::cerr << "Completely unknown exception\n";
throw; // re-throw if you can't handle it
}Derived exception types must be caught before their base classes — catching std::exception before std::invalid_argument would match the derived type in the base-class handler (since derived IS-A base), shadowing the more specific handler. Always put more specific types first.
Custom exception types
Define custom exception classes by inheriting from std::exception or one of its subclasses. Override what()to return a descriptive message. This lets callers catch your library's exceptions specifically.
class DatabaseError : public std::runtime_error {
public:
explicit DatabaseError(const std::string& msg, int code)
: std::runtime_error(msg), errorCode_(code) {}
int errorCode() const { return errorCode_; }
private:
int errorCode_;
};
// Catch specifically:
try {
db.query("SELECT * FROM users");
}
catch (const DatabaseError& e) {
std::cerr << "DB error " << e.errorCode() << ": " << e.what() << "\n";
}noexcept — marking functions that don't throw
The noexcept specifier declares that a function will never throw. If a noexcept function does throw, std::terminate() is called immediately — no stack unwinding. This allows the compiler and standard library to apply optimizations: std::vectorwill use the move constructor instead of the copy constructor only if the move is noexcept.
// Mark functions that cannot throw:
void swap_ints(int& a, int& b) noexcept {
int tmp = a; a = b; b = tmp;
}
// Move operations should always be noexcept when possible:
class Buffer {
public:
Buffer(Buffer&& other) noexcept : data_(other.data_), size_(other.size_) {
other.data_ = nullptr; other.size_ = 0;
}
Buffer& operator=(Buffer&&) noexcept;
// ...
};
// noexcept(expr) — conditional noexcept based on another expression:
template<typename T>
void my_swap(T& a, T& b) noexcept(std::is_nothrow_move_constructible_v<T> &&
std::is_nothrow_move_assignable_v<T>) {
T tmp = std::move(a);
a = std::move(b);
b = std::move(tmp);
}When to use exceptions vs alternatives
Use exceptions when…
- →The error is truly exceptional — not expected in normal operation (file not found, network down, allocation failure)
- →The error must propagate up many layers and the intermediate layers have no way to handle it
- →You are writing a constructor — constructors cannot return error codes
Prefer error codes / std::expected when…
- →Failure is a normal outcome (parsing user input, opening a config file that may not exist)
- →You're in a performance-critical path — exceptions add overhead when thrown
- →You're writing embedded or safety-critical code where exceptions may be disabled
Key rules to remember
Throw by value, catch by const reference
throw MyException(...); and catch (const MyException& e). Catching by value copies the exception and slices derived types to the caught type.
Order catch handlers from most specific to most general
Derived exception types must appear before their base classes. std::invalid_argument before std::logic_error before std::exception.
Mark move constructors and swap functions noexcept
std::vector and other containers use move-or-copy based on noexcept. If your move constructor is not noexcept, vector will copy instead of move on reallocation.
If you catch (...) and cannot handle the exception, re-throw it
A bare throw; inside a catch block re-throws the current exception. Never swallow exceptions you can't handle — it hides bugs.