Exception Handling
cpp1-5 previewed exception safety briefly. This chapter delivers it in full — and closes with the honest, three-way comparison this whole course has been building toward.
C's Total Absence of Error Propagation
Worth naming directly, for the first time: C has no exceptions, no Result type, nothing beyond a manual return code or errno that the caller must remember to check, every single time, with nothing enforcing that check at all.
throw, try, catch
Stack Unwinding
When an exception is thrown, the stack unwinds: every function between the throw site and the matching catch exits, and every local object along the way has its destructor called, in reverse order of construction. This is exactly cpp1-5's own "Exception Safety, Briefly" preview, now shown in full.
RAII's Role in Exception Safety
The real payoff: because destructors run during unwinding, RAII-managed resources — smart pointers, containers, file handles wrapped in RAII types — are cleaned up automatically even when an exception is thrown mid-function, with zero additional code from the programmer. A raw resource acquired with new/malloc and manually freed at a function's end leaks if an exception is thrown before that manual free line is ever reached — a genuine, concrete argument for using RAII wrappers pervasively, not just a style preference.
Standard Exception Types
std::exception is the base type, with std::runtime_error, std::out_of_range, and others derived from it. Catch by reference (const std::exception&) to avoid slicing — a real forward pointer to cpp3-1's own object-slicing material.
Exceptions vs. Return Codes vs. Rust's Result<T, E>
Three real, genuinely different guarantees. C's manual return codes/errno: nothing enforces checking them — silently ignorable. C++ exceptions: an uncaught exception terminates the program rather than silently continuing with a wrong value — genuinely stronger than C — but which exceptions a function might throw is invisible in its own signature, a real, honest downside. Rust's Result<T, E>: the possibility of failure is part of the function's own type, and the compiler forces the caller to at least acknowledge it — via ?, .unwrap(), or a match — genuinely the strongest of the three.
| Concept | C | C++ | Rust |
|---|---|---|---|
| Failure signaling | manual return code / errno | exceptions — throw/catch | Result<T, E> |
| Ignoring failure silently | fully possible — nothing enforces a check | uncaught exception terminates the program | compile-time forced acknowledgment |
| Visible in the function's own type | no | no — throw specifications aren't part of the signature | yes — Result<T, E> is the return type itself |
std::terminate — the entire program crashes outright. Destructors should essentially never throw; a real, important C++ idiom.
Coding Challenges
Write a function that throws a std::runtime_error if a passed-in int is negative, and a try/catch block in main that calls it with a negative value, printing the caught exception's message via e.what().
📄 View solutionWrite a class with a destructor that prints a message, create an instance inside a try block, throw an exception after creating it, and catch the exception in main. Observe when the destructor's message actually prints relative to the catch block.
📄 View solutionExplain precisely why Rust's Result<T, E> is described as the strongest of the three failure-signaling guarantees compared in this chapter, tying your answer to what specifically is and isn't visible in a function's own type signature in each language.
📄 View solutionChapter 6 Quick Reference
throw/try/catch— C++'s error-propagation mechanism, entirely absent from C- Stack unwinding — every local object's destructor runs, in reverse order, between the throw and the catch
- RAII resources clean up automatically during unwinding — a raw resource with manual cleanup leaks on an exception
- Catch by
const std::exception&to avoid slicing (cpp3-1) - C: silently ignorable failure. C++: terminates if uncaught, but invisible in the type signature. Rust: forced compile-time acknowledgment via
Result<T, E> - Destructors should never throw — doing so during unwinding calls
std::terminate - Next chapter: lambda expressions — closures in C++, contrasted with Rust's own