Skip to content

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.

  1. 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).
  2. Validation for every rule, each throw at the boundary of the operation it belongs to.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. A noexcept boundary: the top-level main catches 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 main is 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

  1. Exceptions derive from std::exception (directly or through a standard type), and every one carries the data its caller needs — not just a message.
  2. what() returns something a human can act on, and the structured accessors return the details.
  3. A caller that catches the base type still gets a useful message; a caller that catches a specific kind can react without parsing text.
  4. 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.
  5. The order has an invariant (at least one line, positive quantities) enforced at every entry point.
  6. 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.
  7. Nothing leaks on any throw path: the resource counts at exit are zero.
  8. main catches std::exception at the top, prints the message, and returns 2; an unhandled kind is a test failure, not a crash.
  9. 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.
  10. 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;, never throw e;, and record what the difference is.
  • Every catch takes a reference — searched for and recorded.
  • No std::string matching to distinguish failures anywhere.
  • No warnings.

Starter task

In failure-design.md, before coding:

  1. Fill in the taxonomy table for the four kinds: type, data carried, who throws it, who catches it.
  2. 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.
  3. Write the strong guarantee's demonstration plan: which sequence, and what you will print before and after.
  4. Write your catch-all's message format, and one thing it must not be used to hide.

Implementation tasks

  1. Implement the exception hierarchy with its data and messages.
  2. Implement validation rules, each throwing from one place.
  3. Implement the order builder with the strong guarantee, and demonstrate it.
  4. Implement the three callers, including the batch importer's summary.
  5. Run the resource counts on every path, and record them.
  6. Re-throw in one place with throw; and in another with throw 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:

  1. What does catching by value do to the exception object, and what is lost when the catch is a base class?
  2. Why does the loop stop, and which call would have continued the batch instead?
  3. What does the strong guarantee require around the failing line, and where does the rollback happen?
  4. Which test case in the table would have caught the catch-by-value problem, and what would it print?

Reflection questions

  1. Which failure did you decide not to throw, and what does the caller do instead? What would throwing have cost?
  2. Your exceptions carry data. Why is what() alone insufficient for a caller that wants to react, and how did your design help?
  3. 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?
  4. throw; versus throw e; — what did your experiment show, and why does the type matter?
  5. 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.
  6. 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 -Werror and no warnings
  • [ ] failure-design.md has the taxonomy table and the three non-exception decisions
  • [ ] Every exception derives from std::exception and 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; and throw 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