Module 13 — Exception Handling¶
After this module you can design a program that stays correct when things go wrong — because cleanup is automatic, and because failure is part of the interface rather than an afterthought.
At a glance¶
| Prerequisites | Module 12 |
| Estimated study time | 8–10 hours |
| Lab | Lab 13 — an exception hierarchy, and proof that nothing leaks on either path |
| Exercises | Three tiers — warm-up, core, challenge |
| Cognitive levels | L4 Analyze → L7 Refactor — exit level L6 Build |
| Self-assessment | Module quiz |
Why this module matters¶
Every program has operations that can fail: a file that is not there, an identifier that does not exist, a loan that would exceed the limit, a number the user typed that is not a number. The design question is not whether to handle failure, but how the failure reaches the code that can do something about it.
Error codes fail this test. They are easy to ignore — literally: an ignored return value is often not even a warning — and they force every intermediate function to carry the error upward manually, which means every intermediate function can forget. Exceptions invert that: failure travels upward without any intermediate code asking it to, and the compiler guarantees the cleanup still happens.
That guarantee is not magic. It works because of Module 11: when an exception propagates, every local object's destructor runs. RAII plus exceptions is one mechanism, and this module is where the two halves join.
You will also learn the discipline that keeps exceptions from becoming a menace:
catch by reference, never throw from a destructor, mark what cannot throw as
noexcept, and think about exception safety in terms of what your class
guarantees — basic, strong, or nothrow.
Learning objectives¶
By the end of this module you will be able to:
- Explain what problem exceptions solve that error codes do not.
- Use
throw,tryandcatchcorrectly, catching byconstreference. - Describe propagation and stack unwinding, and state what happens to local objects during unwinding.
- Define a custom exception hierarchy derived from a standard exception type,
with useful
what()messages. - Explain why catching by value slices an exception, and what is lost.
- Use
throw;to rethrow without losing the original exception. - Explain what
noexceptpromises, and identify the consequences of breaking that promise. - Classify an operation's exception safety as basic, strong or nothrow, and state which your own functions provide.
- Prove, with output, that a resource is released on both the normal and the exceptional path.
- Explain why a destructor must not throw, and the standard library's response to a throwing destructor during unwinding.
- Decide, for a given failure, whether an exception or a return value is the better design — and defend the choice.
- Use
catch (...)and explain what it can and cannot tell you.
Core concepts¶
- Exception — an object thrown to signal that an operation could not complete.
throw— raises an exception; control returns to the nearest matching handler, unwinding intermediate frames.try/catch— a block whose exceptions are handled, and the handlers.- Propagation — an exception travelling outward through the call stack until
a matching
catchis found. - Stack unwinding — destroying local objects as the stack is unwound. This is what makes RAII cleanup reliable.
std::exception— the standard base class;what()returns a description. Other standard bases includestd::logic_error/std::runtime_error.- Catch by
constreference —catch (const MyError& e). Catching by value copies the exception and slices derived types. - Custom exception hierarchy — your own base error type, with specific derived errors, so callers can catch broadly or narrowly.
- Rethrow —
throw;with no operand, re-raising the current exception unchanged for an outer handler. catch (...)— catches anything. Useful only for cleanup-and-rethrow or for a boundary; it cannot tell you what went wrong.noexcept— a promise that a function does not throw. If it does, the program callsstd::terminate.- Exception safety guarantees — basic (no leaks, object still valid), strong (operation either succeeds or leaves the object unchanged), and nothrow (never fails).
- Copy-and-swap — the usual way to get the strong guarantee for assignment: build a correct copy, then swap it into place.
- Destructors must not throw — destructors run during unwinding; two
exceptions in flight at once terminate the program. Declaring destructors
noexceptis the default for that reason. - Constructor failure — a constructor that throws means the object never existed, and its destructor will not run. Anything it acquired before throwing must already be owned by a member that will be destroyed properly.
- Error codes vs exceptions — return values remain reasonable for expected, local, high-frequency conditions; exceptions are for conditions the caller usually cannot handle immediately.
Module contents¶
| Section | What it contains |
|---|---|
| Introduction (this page) | Overview, prerequisites, objectives, concept inventory |
| Concepts and explanations | Propagation and unwinding; catching by reference and handler order; a custom hierarchy; RAII on every exit path; the three guarantees; noexcept; and where failure belongs at all |
| Worked examples | Five programs — the same failure as a status and as an exception, an unwinding trace with a counted leak, a hierarchy broken three ways, basic versus strong guarantees, and the decision rules — plus a std::terminate reproduction you run yourself |
| Code examples | examples/module-13/ — five programs, each with captured output showing cleanup on both the normal and the throwing path |
| Misconceptions and mistakes | Eight wrong mental models with the program that contradicts each, sixteen symptoms, and six worked mistakes with their real compiler and runtime messages |
| Debugging | Failure-path forensics: reading a terminate message that names the wrong failure, catch throw with a backtrace, balance sheets, and making the failure happen on purpose |
| Exercises | Three tiers — from one caught exception to a tested failure path that leaves the state unchanged |
| Lab 13 | A custom hierarchy, documented guarantees, and two counter runs: one where everything succeeds, one where an operation throws mid-way |
| Challenge problems | Write an application-wide error-handling policy, refactor to exceptions and argue one case back, and judge a strong implementation against a basic one |
| Self-assessment | Nine questions, including two predictions: a destructor trace, and a malformed file under both guarantees |
| Summary | What must be retained, the terms to be able to define, and the checklist |
| Further reading | Books and official documentation |
What you can do after this module¶
- Define an exception hierarchy that a caller can catch at either the general or the specific level.
- Demonstrate, with counters, that destructors run during unwinding.
- Say which of the basic, strong and nothrow guarantees each of your functions provides.
- Explain why a constructor that throws must not have to clean up by hand.
- Choose between an exception and a returned error for a specific failure, with a reason.
Where this is used later¶
- Exception safety in containers matters when using standard algorithms in
Module 14, especially with
noexceptmoves. - Failures in constructors and factories shape the factory pattern in Module 15.
- Designing for failure across an application — including reading data from files that may be missing or corrupt — is part of the capstone, Module 16.
Common mistakes to watch for¶
| Symptom | Cause |
|---|---|
The message printed by catch is missing derived detail |
Catching by value sliced the exception. Catch by const reference |
| A resource leaks only when an exception occurs | The resource was not owned by an RAII object, so unwinding could not release it |
The program calls std::terminate and dies mid-unwinding |
A destructor threw while another exception was propagating |
A catch block never runs |
Type mismatch — the handler's type does not match the thrown type, and no base class matched either |
error: exception specification of overriding function is more lax than base version |
An override declared noexcept(false) where the base declared noexcept |
After a catch, the program is in an odd state |
The basic guarantee was all that was provided: nothing leaked, but the object may have changed |
| Exceptions used for ordinary control flow | Expensive and hard to follow. Return a value for expected outcomes |
Skills checklist¶
Levels this module works at: [L4 Analyze → L7 Refactor] — see How you will learn.
Tick an item only when the evidence exists:
- [ ] I can throw and catch, and explain what unwinding destroys — evidence: a traced unwinding example, with the destruction order predicted before running it
- [ ] I can write a custom exception hierarchy with useful messages — evidence: three distinct
failures caught distinctly, with
what()naming the item or the id - [ ] I can explain why catching by value is wrong — evidence: the sliced-exception demonstration,
then the
const&fix, plus the-Wcatch-valuewarning quoted - [ ] I can say where RAII does the work and a
catchblock would fail — evidence: the raw-resource leak reproduced and removed by ownership rather than by cleanup code - [ ] I can classify an operation's guarantee and improve it — evidence: one operation documented as strong, with the technique named and the state asserted after a deliberate failure
- [ ] I can explain why a destructor must not throw — evidence: the deliberate termination, its message, and my own one-line rule for every class in my lab
- [ ] I can use
noexceptdeliberately — evidence: at least threestatic_asserts, one of them negative, and a sentence on what the standard library does differently because of it - [ ] I can test a failure path — evidence: Lab 13's second counter run, with the failure produced on purpose and the counts balancing
Continue¶
Previous: Module 12 — Templates · Next: Module 14 — The STL with OOP · All modules: Modules index