Exceptions in C++ propagate and get handled by unwinding the stack: a [[throw]] abandons the current function, runs the destructors of everything it passes on the way out, and looks for a matching [[catch]] further up. The handler that matches is the first one in the nearest enclosing [[try]] block, and everything below it is cleaned up automatically on the way.
Throwing and catching
A try block contains the code that might fail, and one or more catch handlers deal with the failures.
#include
#include
#include
int charge_card(int amount) {
if (amount <= 0) {
throw std::invalid_argument("amount must be positive");
}
if (amount > 1000) {
throw std::runtime_error("amount exceeds the card limit");
}
return amount;
}
int main() {
try {
std::cout << "charging " << charge_card(250) << '\n';
std::cout << "charging " << charge_card(5000) << '\n';
std::cout << "this line never runs\n";
} catch (const std::invalid_argument& error) {
std::cout << "invalid: " << error.what() << '\n';
} catch (const std::exception& error) {
std::cout << "failed: " << error.what() << '\n';
}
std::cout << "checkout continues\n";
return 0;
}The output shows the exception being caught and the program continuing:
charging 250
charging failed: amount exceeds the card limit
checkout continuesThe handler for std::invalid_argument comes first, so it is checked first. The order matters: the more specific handler must be listed before the more general one, or the general one will swallow everything.
The standard exception hierarchy
The standard library provides a family of exception types, all derived from std::exception.
std::exception— the base, with a.what()method returning a message.std::runtime_error— an error that could not have been detected at compile time.std::logic_error— a violation of the program's own logic, such as an invalid argument.std::invalid_argument,std::out_of_range,std::domain_error— specific logic errors.std::overflow_error,std::underflow_error,std::range_error— specific runtime errors.
Catching std::exception by reference catches everything the standard library throws. Catching ... catches everything including exceptions that do not derive from it, and you lose the ability to read a message.
#include
#include
#include
int main() {
try {
throw std::out_of_range("index 5 is past the end");
} catch (const std::exception& error) {
std::cout << "exception: " << error.what() << '\n';
}
try {
throw 42;
} catch (const std::exception& error) {
std::cout << "never printed\n";
} catch (int code) {
std::cout << "caught a plain int: " << code << '\n';
}
return 0;
}The output shows both handlers working, and the second one catching a value that is not an exception object at all:
exception: index 5 is past the end
caught a plain int: 42Propagation through the stack
An exception with no handler in the current function propagates to the caller. It keeps going until something catches it, or until it reaches main and calls std::terminate.
#include
#include
void log_payment() {
std::cout << "logging the payment\n";
}
void process_payment(int amount) {
std::cout << "processing " << amount << '\n';
log_payment();
if (amount <= 0) {
throw std::invalid_argument("amount must be positive");
}
std::cout << "payment accepted\n";
}
int main() {
try {
process_payment(-1);
} catch (const std::exception& error) {
std::cout << "caught: " << error.what() << '\n';
}
return 0;
}The output shows the calls that happened before the throw and the handler that eventually caught it:
processing -1
logging the payment
caught: amount must be positiveNote that "payment accepted" never printed, and the stack was unwound through log_payment and process_payment on the way out.
Defining your own exception type
Deriving from std::exception gives you a type the caller can catch specifically, and .what() lets you carry a message.
#include
#include
#include
class PaymentError : public std::exception {
public:
explicit PaymentError(const std::string& message) : message_(message) {}
const char* what() const noexcept override {
return message_.c_str();
}
int code() const noexcept {
return code_;
}
void set_code(int code) noexcept {
code_ = code;
}
private:
std::string message_;
int code_ = 0;
};
int main() {
try {
throw PaymentError("the payment gateway timed out");
} catch (const PaymentError& error) {
std::cout << "payment error: " << error.what() << '\n';
}
return 0;
}The output shows the custom type being caught specifically:
payment error: the payment gateway timed outThrowing by value and catching by const reference is the convention. Throwing a pointer or catching by value means a copy, and catching by value on a polymorphic type slices the exception.
Rethrowing and the exception_ptr
Inside a catch block you can rethrow the current exception, and you can capture it for later with std::exception_ptr.
#include
#include
#include
int main() {
std::exception_ptr saved;
try {
throw std::runtime_error("temporary failure");
} catch (...) {
saved = std::current_exception();
std::cout << "saved for later\n";
}
try {
std::rethrow_exception(saved);
} catch (const std::exception& error) {
std::cout << "rethrown: " << error.what() << '\n';
}
return 0;
}The output shows the exception being saved and rethrown:
saved for later
rethrown: temporary failureThe cost of exceptions
Throwing is not cheap. It allocates, it unwinds the stack, and it runs every destructor on the way out. A hot loop that throws once per thousand iterations will spend most of its time in the exception machinery.
The practical rule is to use exceptions for the exceptional case — a failed connection, a malformed input, a missing file — and to return a value or a bool for the ordinary case that happens often.
Quick reference
- A
throwunwinds the stack to the nearest matchingcatch, running destructors on the way. - List the most specific handler first; the first match wins.
- Catch by const reference to avoid copying and to avoid slicing.
- Derive custom exceptions from
std::exceptionand override.what(). - Catch
...as a last resort, because you lose the message. - Use exceptions for rare failures, not for ordinary control flow.
Try it yourself: write a function that validates a payment amount and throws [[std::invalid_argument]] for a non-positive value and [[std::out_of_range]] for one above a limit. Catch each specifically in one function and with a single [[std::exception]] handler in another, and compare the messages you can print. Then add a custom exception type that carries an error code as well as a message.