Skip to content

Module 13 — Lab 13: failure as part of the design

Levels: [L4 Analyze → L6 Build] · Extends: Lab 12 · Time: 3–4 hours


The scenario

The library system now has one Repository<T>, smart pointers, and a catalogue that survives being copied. It still assumes that everything works: a title that is not held returns nothing distinguishable from an empty catalogue, a malformed file crashes, and a loan that exceeds the member's limit is silently recorded anyway.

The campus IT office has started running the program against real files, and their bug report is one line long: "it stops without saying why, and twice we had to restart the machine."

You are going to make failure part of the design. Every operation that can fail gets a named, documented failure; every operation's exception-safety guarantee gets written down; and the failure paths get run and counted, not described. That last part is the deliverable a marker would look at first, and it is the part most students skip.


What you start from

Your Lab 12 program. The three operations that need work look like this — each is correct on the success path, which is exactly why the bugs have survived:

class Catalogue {
public:
    bool add(const Item& item);                 // false = could not add. Which failure? Unknown
    std::shared_ptr<Item> find(int id) const;   // nullptr = absent. Or was it an error?
    void loadFrom(const std::string& path);     // aborts on a malformed file

private:
    std::vector<std::shared_ptr<Item>> items_;
};
class LoanLedger {
public:
    void lend(std::shared_ptr<Item> item, const Member& member);
    // Records the loan even when member.overdueCount() >= Member::limit,
    // because nothing in the current design can refuse.
};

Requirements

  1. A custom exception hierarchy rooted in std::exception, with at least three distinct failures thrown from real operations — for example ItemUnavailable (carrying the title), MemberNotFound (carrying the id) and CorruptCatalogue (carrying the file and the line). Each what() names the object involved. (Interacts with Module 8: the hierarchy is itself a small interface design, and a caller must be able to catch it broadly or narrowly.)
  2. Every operation's guarantee is documented in the header, one line each, in the form // Strong guarantee: on failure, the catalogue is unchanged. — no operation left unclassified.
  3. At least one operation is upgraded to the strong guarantee, and the comment names the technique (work on a copy, then swap) and why the last step cannot fail. (Interacts with Module 10: this is copy-and-swap, used as a guarantee.)
  4. No catch block releases a resource. Ownership stays in types. If you find yourself writing a catch that calls close() or reset(), the resource was in the wrong place. (Interacts with Module 11.)
  5. Counters proving balance on the success path and on a path where an exception is thrown mid-operation. This is the requirement that makes the lab real, and it is specified below.
  6. A malformed-input case handled without crashing: a file with one bad line is reported with the file name and the line number, the program continues, and the catalogue's state is unchanged.
  7. A destructor review: one line per class stating why its destructor cannot throw, and what happens to a failure it does discover.
  8. No operation throws from a destructor, and every operation that cannot fail is marked noexcept — checked with a static_assert, not asserted in a comment.
  9. It builds with no warnings using the course's compile command.

The two runs (requirement 5 in detail)

This is the core of the lab. You run the same scripted sequence twice:

Run A - everything succeeds
  load the catalogue from a good file
  lend three items, returning two of them
  print the counters

Run B - one operation fails half-way
  load the catalogue from a good file
  start a lend that will fail (member at the limit, or an item already out)
  catch the failure, report it, continue
  return the items, save, print the counters

And the counters must balance in both:

Run A: acquisitions 14   releases 14   live 0
Run B: acquisitions 13   releases 13   live 0   (and 1 refusal, which acquired nothing)

An operation that fails after acquiring something is where a leak would show, so Run B must fail at the worst possible moment — not before the acquisitions. Make the failure happen on purpose, at a line you chose:

if (failAtItem == index) {
    throw ItemUnavailable("simulated failure at item " + std::to_string(index));
}

If the counters differ between your two runs, the difference is the bug: it names how many acquisitions were not released, and the unwinding trace names which one.


What to deliver

  • The program, with the hierarchy, the documented guarantees and both runs available from the command line (./library --run-a and ./library --run-b, or two entry points), each printing its counters.
  • The transcript of both runs, pasted side by side or one after the other.
  • A short table with one row per operation: what it throws, what guarantee it provides, how the failure was tested. An untested claim belongs in the table as a gap, not as a tick.
  • One paragraph on the failure you changed from throwing to returning a value, with the reason — because the right answer is not always an exception, and the module's judgement exercise is worth more than another throw.

Self-assessment checklist

It builds and runs

  • [ ] Compiles with g++ -std=c++17 -Wall -Wextra -Werror with no warnings
  • [ ] Both runs complete and print their counters

It is correct

  • [ ] Boundary cases work: an empty catalogue, a single item, a member at the limit, a missing id
  • [ ] The malformed file is reported (file name and line) and the program continues
  • [ ] Nothing leaks on the failure path: the counters balance in both runs
  • [ ] The strong-guarantee operation leaves the state unchanged when it fails, and this is asserted

It is designed

  • [ ] Every operation's guarantee is written next to its declaration
  • [ ] The hierarchy is shallow and each level has a reason to exist
  • [ ] No catch block releases a resource
  • [ ] Destructors cannot throw, and the one that might fail says what it does instead

It is verified

  • [ ] The failure was produced deliberately, at a known line — not awaited
  • [ ] I tested the failure path, not only the success path (this module's substance)
  • [ ] Diagnostics are on std::cerr, so an abort cannot swallow them
  • [ ] I can explain why the second run's counter balance is stronger evidence than the first

Common ways this lab goes wrong

Symptom Cause
The counters balance in Run A and not in Run B A resource is not owned by an object; look for a raw handle acquired in a constructor body or released by a statement
The catch block never runs A handler is ordered after a base class that already matched, or catch (...) above both
The malformed file ends the program The exception escaped main; there is no boundary handler, or the load path throws something not derived from std::exception
The program terminates with a message naming a destructor A destructor throws. Report, do not throw
error: looser exception specification on overriding virtual function An override throws where the base declared noexcept — the base's contract has to be changed deliberately, and every caller revisited
warning: 'throw' will always call 'terminate' [-Wterminate] A throw inside a noexcept function or a default destructor. Under -Werror this is a stop, which is the point
The strong-guarantee operation still mutates on failure Validation happens after the mutation. Validate first, or work on a copy and swap
The state check after the failure is missing Requirement 3 is a claim about behaviour; without the assertion it is only a comment

Reference solution

No solution file is published for this lab. It is a system whose failure paths must be as considered as its success path, and the decisions are the work: what is refused, what is thrown, what is left unchanged, and where an exception is the wrong report.

The model code is in examples/module-13/, one program per idea, each already walked through on the concepts page where it is used:

Program What it models
01-error-codes-versus-exceptions The same failure reported three ways, and what each forces on the caller
02-unwinding-and-cleanup What an exception does on its way out of a function
03-custom-hierarchy-and-slicing Designing an exception hierarchy, and the three mistakes that make it useless
04-guarantees-and-noexcept The three exception-safety guarantees, and the promise noexcept makes
05-validation-and-design-rules The decision this module builds towards: throw, return, or refuse

Two things are worth comparing against your own version in particular: the counter output for both runs, which is the evidence that nothing leaked on either path, and any operation where you chose between throwing and returning a value.


Continue

Previous: Exercises · Next: Challenge problems · Module home: Module 13