Skip to content

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

void load() noexcept { throw std::runtime_error("file missing"); }   // promise broken
terminate called after throwing an instance of 'std::runtime_error'
  what():  file missing

The try/catch around the call does not help: the exception never leaves the function.

3. Catching by value — slicing

catch (std::runtime_error e)      // by value
error: catching polymorphic type 'class std::runtime_error' by value [-Werror=catch-value=]

And inside that handler, the derived part is gone:

error: 'class std::runtime_error' has no member named 'line'

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 const reference?
  • [ ] Are handlers ordered most-derived first?
  • [ ] Is there any throw inside a destructor?
  • [ ] Is there a noexcept function 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