Exceptions: throw, catch, unwind
Exceptions, STL, and ownership13 min readLesson 1 of 4
1.1 Errors that need a different path
A function can report failure with a return code, and callers can ignore it. A constructor cannot return a status at all, and a chain of calls can fail several levels below the code that knows how to respond. Exceptions move the failure to a handler without threading an error value through every function.
A throw creates an exception object and transfers control to the nearest matching catch. Code between the throw and the handler does not run to completion; its local objects are destroyed as the stack unwinds. The handler then decides whether the program can continue.
#include <iostream>
#include <stdexcept>
int checked_age(int age) {
if (age < 0) {
throw std::runtime_error("age cannot be negative");
}
return age;
}
int main() {
try {
std::cout << checked_age(20) << "\n";
std::cout << checked_age(-1) << "\n";
} catch (const std::runtime_error& error) {
std::cout << "Error: " << error.what() << "\n";
}
return 0;
}Output
20 Error: age cannot be negative
1.1.1 Matching a handler
A try block marks code whose exceptions this handler covers. Each catch names a type, and the runtime selects the first handler whose type matches the exception or a base of it. Order the handlers from the most specific type to the most general, and put catch (...) last.
Catch by const reference. Catching by value copies the exception and can slice a derived exception into its base type. Throwing by value and catching by reference is the usual pairing. An exception with no matching handler calls std::terminate, so a program that throws must have a plan for it.
#include <iostream>
#include <stdexcept>
class Guard {
public:
explicit Guard(const char* name) : name_(name) { std::cout << "enter " << name_ << "\n"; }
~Guard() { std::cout << "leave " << name_ << "\n"; }
private:
const char* name_;
};
void run() {
Guard guard("run");
throw std::runtime_error("stop");
}
int main() {
try { run(); }
catch (const std::exception&) { std::cout << "caught\n"; }
return 0;
}Output
enter run leave run caught
1.1.2 Exceptions during construction
A constructor that detects invalid arguments can throw. The object is then considered never constructed, so its destructor does not run. Data members that were fully constructed before the throw are destroyed, and base classes are destroyed in reverse order.
Raw resources acquired in the constructor body before the throw can leak, because no destructor cleans them up. Members that own resources through RAII solve that problem: their destructors run during unwinding. Prefer std::string, std::vector, and smart pointers as members so a failed constructor leaves nothing behind.
#include <iostream>
#include <stdexcept>
class Account {
public:
explicit Account(int balance) : balance_(balance) { if (balance_ < 0) throw std::invalid_argument("negative balance"); }
int balance() const { return balance_; }
private:
int balance_;
};
int main() {
try {
Account good(50);
std::cout << "Balance: " << good.balance() << "\n";
Account bad(-5);
} catch (const std::invalid_argument& error) {
std::cout << "Rejected: " << error.what() << "\n";
}
return 0;
}Output
Balance: 50 Rejected: negative balance
1.1.3 Standard exception types
std::exception is the base class for the standard exception hierarchy. std::runtime_error covers errors detected at runtime, std::logic_error covers programming mistakes, and std::invalid_argument and std::out_of_range are derived from logic_error. std::vector::at and std::string::substr throw std::out_of_range when a position is invalid.
A custom exception can derive from std::runtime_error, pass a message to its constructor, and add data such as an error code. It can then be caught by its own type or by std::exception. Keep the message short and put machine-readable details in members.
#include <iostream>
#include <stdexcept>
#include <vector>
int main() {
std::vector<int> marks = {70, 80};
try {
std::cout << "Mark: " << marks.at(1) << "\n";
int missing = marks.at(5);
std::cout << "Mark: " << missing << "\n";
} catch (const std::out_of_range&) {
std::cout << "Index out of range\n";
}
return 0;
}Output
Mark: 80 Index out of range
1.1.4 A custom exception type
Deriving from a standard exception keeps the catch sites familiar and lets what() return a message. The derived constructor forwards the message to the base constructor and initializes its own fields in the initializer list.
Catch the custom type by const reference when the handler needs its extra data, and catch std::exception at a boundary that only logs or reports. A class used as an exception should be copyable, because the runtime may copy the thrown object.
#include <iostream>
#include <stdexcept>
class InsufficientFunds : public std::runtime_error {
public:
explicit InsufficientFunds(int shortfall)
: std::runtime_error("insufficient funds"), shortfall_(shortfall) {}
int shortfall() const { return shortfall_; }
private:
int shortfall_;
};
int main() {
try {
throw InsufficientFunds(30);
} catch (const InsufficientFunds& error) {
std::cout << error.what() << ": " << error.shortfall() << "\n";
}
return 0;
}Output
insufficient funds: 30
1.1.5 Exception safety and destructors
Code that throws leaves objects in some state. The basic guarantee says the program remains valid and resources are released. The strong guarantee says a failed operation leaves the object unchanged, which usually means doing the work on a copy and swapping at the end.
Destructors should not throw. They often run during unwinding, and a second active exception calls std::terminate. The standard library marks most destructors noexcept for this reason. Use RAII so cleanup does not depend on a catch block running.
1.1.6 When an exception is the wrong tool
An exception reports something unexpected that prevents an operation from finishing. A search that finds no result, an optional setting that is absent, or a user who cancels an action are ordinary outcomes, and they read better as a return value. C++17 offers std::optional<T> for a value that may be absent.
An exception also costs more than a returned value, and a handler far from the throw can hide the control flow. Use exceptions at boundaries where the failure cannot be handled locally, and document the exceptions a function may throw. For functions that never throw, noexcept lets the compiler and the standard library optimize around that fact.
1.1.7 Common mistakes and practice
Mistakes to watch for:
- Catching by value, which can slice a derived exception.
- Swallowing every exception with an empty
catch (...)block. - Throwing a pointer to a local object.
- Letting a destructor throw during unwinding.
- Using exceptions for ordinary branching such as a missing optional value.
Practice:
- Write a function that throws
std::runtime_errorwhen a number is negative and catch it inmain. - Print from a local object's destructor to watch stack unwinding.
- Make a constructor reject a negative balance and catch the failure in
main. - Catch
std::out_of_rangefromstd::vector::at. - Add a custom exception with an error code and print both the message and the code.
