Exceptions and Error Handling
7 pages · ~5 min✓ Reviewed
Builds on Memory, RAII and Smart Pointers. Next up: STL Containers and Algorithms.
Exceptions: The Mental Model
1. Throw Transfers Control
throwjumps up the call stack to the nearest matching catch- The current function stops immediately
- No normal return happens
- throwfunction stops
- Unwindleave callers
- Matchnearest catch
- Handler runs
2. Handler Form
- Handlers tried top to bottom, first match wins
- List derived before base
catch (...)takes anything, so put it last- Bare
throw;rethrows the original object
cpp
try { run(); } catch (const std::invalid_argument& e) { log(e.what()); } catch (const std::exception& e) { log(e.what()); } catch (...) { throw; }
3. Style and Failure
| Do | Avoid | |
|---|---|---|
| Throw | by value: throw std::runtime_error("x"); | extra copies |
| Catch | const std::exception& | by value: slices |
| Rethrow | throw; | throw e; copies, slices |
Uncaught
Uncaught exception calls std::terminate, which aborts. Even main() with no handler terminates, without guaranteed full unwinding.
Standard Exception Hierarchy and Custom Types
1. The Hierarchy
- std::exception is the root
- Virtual
what()returnsconst char* - Two subclasses:
logic_error,runtime_error
| Branch | Members | Flags |
|---|---|---|
| logic_error | invalid_argument, out_of_range, length_error, domain_error | Preventable programmer or contract bugs |
| runtime_error | range_error, overflow_error, underflow_error, system_error | Failures found only at runtime |
2. Other Library Throws
| Exception | Thrown by |
|---|---|
bad_alloc | new |
bad_cast | dynamic_cast on references |
bad_optional_access | optional without a value |
noexcept and Exception Safety Guarantees
1. noexcept
- noexcept = no exception escapes; if one does,
std::terminateruns, unwinding not guaranteed noexcept(expr)is the conditional form;noexcept(f())is the query operator- Destructors are implicitly noexcept; a throw during unwinding terminates, so catch inside
- Moves must be noexcept:
vectorgrowth usesmove_if_noexcept, else slow copying
cpp
void f() noexcept;
template <class T>
void g(T& t) noexcept(noexcept(t.run()));
static_assert(noexcept(f()));conditional noexcept wrapping the query operator
2. Four Guarantees
| Guarantee | Promise | Typical |
|---|---|---|
| None | leaks or corrupt state | buggy code |
| Basic | invariants hold, no leaks, state valid but unspecified | most code |
| Strong | commit or rollback, state unchanged | copy-and-swap |
| Nothrow | never fails | swap, destructors |
3. Copy-and-Swap
RAII and Stack Unwinding
1. RAII
- RAII: acquire in the constructor, release in the destructor
- Resource lifetime equals object lifetime
| Resource | RAII type |
|---|---|
| Heap | unique_ptr, make_unique, shared_ptr |
| Mutex | lock_guard, scoped_lock |
| File | fstream |
| Memory | vector, string |
2. Stack Unwinding
- 1throw
- 2Leave scope
- 3Destroy localsreverse order
- 4Reach catch
- Fully built locals are destroyed in reverse order of construction
- Constructor throws: its destructor does not run; built members and bases are destroyed
- Raw
newthen a throw leaks: make members RAII
3. Mistakes
Error Codes vs Exceptions vs std::expected
1. Which Tool When
| Exceptions | Error codes / returns | |
|---|---|---|
| Use for | rare, not locally handled: ctor fail, bad_alloc | frequent: parse or lookup miss, hot paths, -fno-exceptions |
| Ignored? | never silently | yes, so mark [[nodiscard]] |
| Happy path | zero cost | zero cost |
| Failure path | slow, ~µs: unwinding + RTTI | cheap |
2. std::expected (C++23)
std::expected<T,E>holds either a value or an error- Check
if (r), then*rorr.value();r.error()gives the error
cpp
std::expected<int, Err> parse(std::string_view s) {
if (s.empty())
return std::unexpected(Err::NotFound);
return std::stoi(std::string(s));
}
auto r = parse(s);
if (r) use(*r); else log(r.error());- Chain without nested ifs:
.and_then(f),.transform(f),.or_else(f)
Exception Handling Cheat Sheet
1. Syntax and Hierarchy
throw X(...)by value;catch (const X& e)by const refcatch (...)goes last; rethrow withthrow;throw e;copies and slices: never use it
cpp
try { risky(); } catch (const std::exception& e) { log(e.what()); throw; } catch (...) { throw; }
| Root | Branch | Types |
|---|---|---|
| std::exception | logic_error | invalid_argument, out_of_range |
| runtime_error | overflow_error, system_error | |
| direct | bad_alloc, bad_cast |
2. noexcept and Guarantees
- Destructors and moves are noexcept
- A throw out of
noexceptmeans terminate swapis nothrow
Check yourself
What does this print?
~B x- Unwinding destroys
bbefore the handler runs
struct B { ~B() { std::cout << "~B "; } };
void f() { B b; throw std::runtime_error("x"); }
try { f(); } catch (const std::exception& e) { std::cout << e.what(); }Spot the bug: catch (const std::exception& e) { log(); throw e; }
throw e;copies and slices to plainstd::exception- Use a bare
throw;to keep the original object
Does this leak? S() : p(new int[4]) { throw 1; } with delete[] p in ~S
- Yes: the constructor never finished, so
~Snever runs - Make
pastd::unique_ptr<int[]>: members are destroyed