Exception Handling Cheatsheet¶
An exception is a failure that the caller must handle. It is not a return code with a different syntax, and it is not a control-flow device. This sheet is the syntax, the guarantees, and the four ways exceptions go wrong.
Pairs with Module 13 — Exception Handling
The syntax¶
#include <stdexcept>
void withdraw(int amount)
{
if (amount <= 0) { throw std::invalid_argument("amount must be positive"); }
if (amount > balance_) { throw std::logic_error("insufficient balance"); }
balance_ -= amount;
}
try
{
account.withdraw(amount);
}
catch (const std::logic_error& e) // most derived first
{
std::cerr << "refused: " << e.what() << '\n';
}
catch (const std::exception& e) // by reference, always
{
std::cerr << "failed: " << e.what() << '\n';
}
| Do | Do not |
|---|---|
| Throw a type that says what went wrong | Throw int, const char* or std::string |
Catch by const reference |
Catch by value (slices) or by raw pointer (leaks) |
Derive from std::exception so generic handlers work |
Invent a hierarchy with no common base |
Put what() text in the exception |
Print inside the library; the caller decides |
| Order handlers most-derived first | Catch ... except at a boundary, for cleanup and rethrow |
A useful exception hierarchy¶
class AppError : public std::runtime_error
{
public:
explicit AppError(const std::string& what) : std::runtime_error(what) {}
};
class ParseError : public AppError { public: using AppError::AppError; };
class FileError : public AppError { public: using AppError::AppError; };
class RangeError_ : public AppError { public: using AppError::AppError; };
Carry context, not just a sentence. An error a user can act on names the line, the field and
the offending text: throw ParseError("line 12: expected 4 fields, found 3");.
The three guarantees¶
| Guarantee | Promise | Typical implementation |
|---|---|---|
| Basic | No leaks; every object is still valid, though its value may have changed | The minimum acceptable for any operation |
| Strong | The operation either succeeds or leaves everything exactly as it was | Do the work on a copy, then swap; or validate everything first |
| Nothrow | It cannot fail — destructors, swap, moves marked noexcept |
noexcept on the function |
// Strong guarantee, by construction: build it, then swap it in
void Library::loadAll(std::istream& in)
{
std::vector<Book> loaded = parseAll(in); // may throw; "books_" is untouched
books_.swap(loaded); // cannot throw
}
Why it matters in practice: a student project that half-loads a file leaves the program in a state no one designed, and the bug appears later, somewhere else.
RAII is what makes cleanup reliable¶
Stack unwinding destroys every fully-constructed local object as the exception travels. That is the whole reason to hold resources in objects:
void process()
{
std::ifstream in("data.txt"); // closed on every path, including a throw
auto buffer = std::make_unique<char[]>(4096); // released on every path
parse(in); // may throw
}
Naked new/delete pairs are not exception-safe unless the new happened in an already
successful statement and the delete is inside a destructor.
Four ways exceptions go wrong, with the real message¶
1. Throwing from a destructor¶
error: 'throw' will always call 'terminate' [-Werror=terminate]
note: in C++11 destructors default to 'noexcept'
Destructors are noexcept by default, so an escaping exception calls std::terminate. The program
dies with no handler able to intervene. Give the type an explicit close() that reports failures,
and let the destructor be a best-effort cleanup.
2. Throwing from a noexcept function¶
The try/catch around the call does not help: the exception never leaves the function.
3. Catching by value — slicing¶
And inside that handler, the derived part is gone:
e.what() may still work (it is virtual and the object was sliced from a complete one), but every
member the derived type added has been discarded. Always catch (const std::exception& e).
4. Using exceptions for ordinary control flow¶
Looking up a missing key in a map, validating user input, or ending a search loop are not failures.
An exception per iteration is slow, hard to read, and hides the failures that are real. Prefer a
return value, bool, std::optional, or an outcome type for expected outcomes.
Uncaught exceptions, and what the user sees¶
terminate called after throwing an instance of 'std::out_of_range'
what(): vector::_M_range_check: __n (which is 5) >= this->size() (which is 2)
If you see terminate called, an exception reached the top with no handler. The message tells you
the type and what(); finding the throw is your job, and the
Debugging Checklist has the procedure.
Checklist¶
- [ ] Does every failure path either return a clear value or throw a typed exception — never silently continue?
- [ ] Is every catch by
constreference? - [ ] Are handlers ordered most-derived first?
- [ ] Is there any
throwinside a destructor? - [ ] Is there a
noexceptfunction that can throw? - [ ] Is the strong guarantee provided where the caller needs it — and tested?
- [ ] Does a failed load leave the object untouched, and does a test prove it?
- [ ] Are exceptions reserved for failures rather than for normal flow?
Related: Smart Pointers · Common OOP Bugs · Debugging Checklist · Module 13 — Exception Handling