Practice Lab 25 — Order validation with an exception hierarchy¶
Difficulty: Advanced · Estimated time: 5–6 hours · Module 13: Exception Handling and Robust OOP
The lab where "something went wrong" becomes a hierarchy of specific failures, and where you have to decide, for each failure, whether it is an exception at all.
At a glance¶
| Difficulty | Advanced |
| Estimated time | 5–6 hours |
| Module | Module 13 — Exception Handling and Robust OOP |
| Concepts practised | Throwing and catching by reference, exception hierarchies, standard exceptions, exception safety, RAII under unwinding, validation design, exceptions versus return codes |
| Prerequisite | Practice Lab 24 |
| Class design | yours — with the failure taxonomy given |
Scenario¶
A factory's shop floor orders materials. An order has a customer, a list of items with quantities, a delivery date, and a state. Validation has four kinds of failure, and they are not the same kind of thing:
- Invalid data — a negative quantity, an unknown item code, a delivery date in the past. The caller made a mistake, and no amount of retrying will help.
- Refused by policy — the customer is over their credit limit, or the item is not sold in that quantity. Legitimate, expected, and a normal outcome.
- Unavailable — the item exists but is out of stock, with a restock date. Expected, and temporary.
- Internal failure — a lookup that cannot find an item that the catalogue promised to contain. This is a programming error, and pretending it is a user error hides a bug.
The previous system threw one std::runtime_error with a different string for each, and the caller matched
on the string. This lab replaces that with a design.
Problem statement¶
Implement order validation with a designed failure taxonomy.
- An exception hierarchy for the four kinds above, deriving from an appropriate standard exception, with anything the caller needs to react (the failing item code, the available quantity, the restock date).
- Validation for every rule, each throw at the boundary of the operation it belongs to.
- A decision recorded for each failure kind: exception, return value, or assertion. Write it in
failure-design.md, and make the code match — including for the cases where you decided not to throw. - Three callers with different needs: a batch importer that keeps going after a refusal, a command line that reports the first failure to a human, and a test that asserts on the failure.
- Weak and strong guarantees: the order builder gives the strong guarantee when a validation fails part way through adding a batch (the order is unchanged); demonstrate it with a before/after comparison.
- Proof of no leak on the exception path: the order owns its items through smart pointers or by value, and the counts at exit are zero.
- A
noexceptboundary: the top-levelmaincatches everything, prints a message and returns a non-zero status, and nothing escapes.
In failure-design.md:
- The taxonomy table: kind, exception type (or the alternative you chose), what the caller can do, and where it is thrown from.
- The three failures you decided not to model as exceptions, and why.
- The strong guarantee's demonstration, with the before/after counts.
- What the catch-all in
mainis for, and one thing it must not be used for.
Learning objectives¶
By the end of this lab you will be able to:
- Design a small exception hierarchy that a caller can catch selectively and act on.
- Catch by reference, and explain why catching by value slices.
- Choose between exceptions, return values and assertions for each failure kind, and defend the choice.
- Provide the strong guarantee for a multi-step operation, and demonstrate it.
- Show that resources are released when an exception unwinds past them.
Functional requirements¶
- Exceptions derive from
std::exception(directly or through a standard type), and every one carries the data its caller needs — not just a message. what()returns something a human can act on, and the structured accessors return the details.- A caller that catches the base type still gets a useful message; a caller that catches a specific kind can react without parsing text.
- No exception is used for a condition the caller is expected to handle routinely if you decided a return value serves better — and the notes say which and why.
- The order has an invariant (at least one line, positive quantities) enforced at every entry point.
- Adding a batch is atomic: a failure leaves the order exactly as it was — demonstrated by printing the line count and total before and after.
- Nothing leaks on any throw path: the resource counts at exit are zero.
maincatchesstd::exceptionat the top, prints the message, and returns 2; an unhandled kind is a test failure, not a crash.- The batch importer reports a summary: how many lines were added, and how many were refused with which kind — so the taxonomy is visibly useful.
- A custom exception is caught by its base class in one place and by its own type in another, and both behaviours are demonstrated.
Constraints¶
- Header and source per type family, with a documented build command.
- Rethrow with
throw;, neverthrow e;, and record what the difference is. - Every catch takes a reference — searched for and recorded.
- No
std::stringmatching to distinguish failures anywhere. - No warnings.
Starter task¶
In failure-design.md, before coding:
- Fill in the taxonomy table for the four kinds: type, data carried, who throws it, who catches it.
- Decide for each of these three whether it is an exception: an unknown item code during import; a quantity above the stocked maximum; a catalogue lookup that fails for an item that should exist. Argue each.
- Write the strong guarantee's demonstration plan: which sequence, and what you will print before and after.
- Write your catch-all's message format, and one thing it must not be used to hide.
Implementation tasks¶
- Implement the exception hierarchy with its data and messages.
- Implement validation rules, each throwing from one place.
- Implement the order builder with the strong guarantee, and demonstrate it.
- Implement the three callers, including the batch importer's summary.
- Run the resource counts on every path, and record them.
- Re-throw in one place with
throw;and in another withthrow e;, and record what changes.
Test cases¶
| # | Action | Expected result |
|---|---|---|
| 1 | Add a negative quantity | The invalid-data kind; the message names the value |
| 2 | Add an unknown code during import | The batch importer refuses the line and continues |
| 3 | Order above the credit limit | The policy kind, with the limit and the total |
| 4 | Out-of-stock item with a restock date | The unavailable kind, carrying the date |
| 5 | A missing catalogue entry that should exist | The internal kind; a test asserts it is thrown |
| 6 | Catch the base type | A useful what(), no parsing |
| 7 | Catch the specific unavailable kind | Access the restock date without casting |
| 8 | Add a batch where the third line is invalid | Strong guarantee: the order's lines and total are unchanged |
| 9 | The same batch with the invalid line removed | All lines added; the total matches |
| 10 | Counts on a throw path | Zero resources alive at exit |
| 11 | throw; versus throw e; |
The difference recorded, with the type that arrives |
| 12 | Top-level catch | Message printed, exit status 2, nothing escapes |
| 13 | Search for catch (.*) by value |
Nothing found |
Boundary cases to run deliberately: a quantity of exactly the maximum; an order of one line; a batch with every line invalid; a delivery date of today (not the past).
Expected behaviour¶
add -3 of B-1 -> InvalidQuantity: quantity -3 is not positive (item B-1)
import 3 lines, line 2 invalid:
line 1 added
line 2 refused: InvalidQuantity (quantity 0 is not positive)
line 3 added
summary: 2 added, 1 refused [InvalidQuantity 1]
batch with an invalid third line -> order unchanged
before: lines 2, total 9000
after: lines 2, total 9000 <- strong guarantee
order over the limit -> OverCreditLimit: 12000 exceeds the limit 10000
catch by base: what() = "quantity 0 is not positive (item B-1)"
catch by kind: restock date 20260501
resources alive at exit: 0
exit status: 2
Extension challenge¶
Add error codes back as a second channel for one specific failure — the refused-by-policy case — and keep the exception for the rest.
Record in failure-design.md:
- Why this one failure is better as a return value here, and what the caller must now check.
- What the importer's summary loses and gains when a refusal arrives as a value rather than an exception.
- The mixing cost: a caller that must now handle two channels for the same operation.
- The change cost: files, operations, lines, and which callers had to change.
Debugging challenge¶
The batch importer stops at the first refusal, and the catch block reports the wrong kind:
try {
for (const auto& line : lines) {
order.add_line(line); // throws InvalidQuantity for line 2
++added;
}
} catch (OrderException e) { // by value
std::cout << "refused: " << e.what() << "\n";
}
Reproduce it — import the three-line batch with an invalid second line — and answer:
- What does catching by value do to the exception object, and what is lost when the catch is a base class?
- Why does the loop stop, and which call would have continued the batch instead?
- What does the strong guarantee require around the failing line, and where does the rollback happen?
- Which test case in the table would have caught the catch-by-value problem, and what would it print?
Reflection questions¶
- Which failure did you decide not to throw, and what does the caller do instead? What would throwing have cost?
- Your exceptions carry data. Why is
what()alone insufficient for a caller that wants to react, and how did your design help? - The strong guarantee needed something to hold the new state before committing it. What did you use, and what would have happened with the basic guarantee?
throw;versusthrow e;— what did your experiment show, and why does the type matter?- The internal failure is a programming error. Is an exception the right response, or should it be an assertion? Answer with what each does in a release build.
- The batch importer uses the taxonomy to summarise. Which kind of failure would you not want it to continue past, and where is that rule enforced?
Completion checklist¶
- [ ] It builds from a documented command with
-Wall -Wextra -Werrorand no warnings - [ ]
failure-design.mdhas the taxonomy table and the three non-exception decisions - [ ] Every exception derives from
std::exceptionand carries structured data - [ ] Catches are by reference throughout — searched for and recorded
- [ ] No string matching to distinguish failures
- [ ] The strong guarantee is demonstrated with before/after counts
- [ ] No leak on any throw path — counts are zero
- [ ]
throw;andthrow e;were compared, with the result recorded - [ ] All thirteen test cases run, with output recorded
- [ ] The top-level catch prints a message and returns non-zero
- [ ] I can state, for each failure kind, who throws it and who is expected to catch it
Related: Lab index · Module 13 — Exception Handling · Previous: Practice Lab 24 · Next: Practice Lab 26 — Failure-safe import with the strong guarantee