Skip to content

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:

  1. Explain what problem exceptions solve that error codes do not.
  2. Use throw, try and catch correctly, catching by const reference.
  3. Describe propagation and stack unwinding, and state what happens to local objects during unwinding.
  4. Define a custom exception hierarchy derived from a standard exception type, with useful what() messages.
  5. Explain why catching by value slices an exception, and what is lost.
  6. Use throw; to rethrow without losing the original exception.
  7. Explain what noexcept promises, and identify the consequences of breaking that promise.
  8. Classify an operation's exception safety as basic, strong or nothrow, and state which your own functions provide.
  9. Prove, with output, that a resource is released on both the normal and the exceptional path.
  10. Explain why a destructor must not throw, and the standard library's response to a throwing destructor during unwinding.
  11. Decide, for a given failure, whether an exception or a return value is the better design — and defend the choice.
  12. 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 catch is 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 include std::logic_error/std::runtime_error.
  • Catch by const reference — 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 calls std::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 noexcept is 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 noexcept moves.
  • 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-value warning quoted
  • [ ] I can say where RAII does the work and a catch block 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 noexcept deliberately — evidence: at least three static_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