Skip to content

Module 13 — Challenge Problems

Open problems. More than one defensible answer exists, and the reasoning is assessed as much as the code.

All three of these are about judgement: which failures deserve a mechanism, what a guarantee is worth paying for, and when the modern answer is the wrong one. Write your decision and its cost before you compare with anything else.


Challenge 1 — Write the error-handling policy for a whole application [L5 Design]

The problem. You have four mechanisms available: validate at the boundary, return a value, throw, and assert. Write the policy for the library system — as a short document a new contributor could apply without asking you — and then make the code obey it.

Your policy must answer, in advance:

  • Which data is untrusted, and where exactly is it validated? (Name the functions.)
  • Which failures throw, and which return? Give the rule, not a list — a list of exceptions is a coincidence, not a policy.
  • What kind of failure is a caller error (assert / std::logic_error), and how is it distinguished from bad data that a user typed?
  • What does main catch, what does it print, and what exit status does it return?
  • What is never allowed anywhere: a destructive catch (...), a throwing destructor, a catch that releases a resource, a what() that does not identify the object.

What a good answer contains.

  • Four named categories with a rule each, so a new failure can be classified without you. "ItemUnavailable throws, find returns" is a list; "the operation cannot do what was asked, and the immediate caller cannot repair it" is a rule.
  • One decision that contradicts the obvious choice, justified. For example: a failing lookup that happens in a tight loop returns; a failing lookup that happens once, deep, throws — even though both are "not found".
  • Consistency, applied. Every operation in the system classified, including the boring ones, and the table from the lab filled in with no gaps.
  • The cost named. Validation at the boundary means the data is checked once and trusted afterwards; that trust is now an assumption someone must maintain, and the policy should say where the boundary is so that assumption stays true.
  • What the policy deliberately does not cover. Anything you left undecided (a network call that does not exist yet, a database that is not in this system), named as a gap rather than pretended away.

What would make it weak. "Throw everywhere, catch in main" — which is a policy in the same way that "print something" is a user interface. The giveaway is a policy with no return category: a search that finds nothing is not an exceptional condition, and a policy that cannot express that will push students into using exceptions for control flow.

Deciding it for yourself. Apply your own rules to two operations you would rather not classify — a comparison operator, and a print function. If the policy gives no answer, or an answer you would not follow, the rule is too vague.


Challenge 2 — Refactor to exceptions, then argue against yourself [L7 Refactor]

The problem. Take a program that handles failure entirely with return codes — your Lab 1 baseline works, or write a 60-line one — and refactor it to exceptions. Then name one place where the return code was genuinely better, and defend it with the categories from section 10 of the concepts page.

What a good answer contains.

  • A refactor that measurably simplified the middle. The evidence is the intermediate frames: after the change, how many of them still mention the failure at all? Before-and-after line counts are legitimate evidence here, and honest.
  • The caller's view, both ways. Show one call site where the exception version is shorter and one where it is not — because a caller that must handle three different failures in three different ways can end up with a handler list longer than a status check would have been.
  • The exception to the rule, with a concrete reason. The strongest answers name one of: an expected and frequent outcome (a search, a cache miss, an optional lookup); a callback whose failure belongs to its caller; a hot path where throwing would be control flow; or a noexcept boundary where a failure must be reported without leaving the function.
  • One thing you would have done differently with hindsight — a throw you later converted back to a return, or a validation you added at the boundary only after the third bug.

What would make it weak. A refactor that improved the middle and worsened the boundaries: three hundred lines of try/catch distributed through main, one handler per menu option, all of them printing the same message. The refactor's whole purpose is to make the failure travel to somebody who can act; if nobody can act, the code has just moved the if statements.

Deciding it for yourself. For each failure you converted, ask: how often does this happen in a normal run, and does the caller usually do something about it here? Frequent-and-handled-locally belongs to a returned value. Rare-and-handled-far-away belongs to an exception. Neither answer is a style preference, and both can be defended in the same program.


Challenge 3 — Choose between a strong and a basic implementation, and say which you ship [L8 Evaluate]

The problem. Two implementations of the same catalogue operation are in front of you. addStrong works on a copy and swaps, so it is commit-or-rollback at the cost of one full copy of the catalogue per call. addBasic pushes an item and then validates, so a failure can leave the new item in place — and every caller must re-read whatever it depended on. Judge them.

Assess, in writing:

  • Correctness — what can a caller conclude after a failure, in each case? Which callers in your lab program would actually be wrong if they assumed the wrong one?
  • Cost — the copy is proportional to the catalogue. What does that mean for a catalogue of ten items, of ten thousand? Would the answer change if the operation ran once per user action, or once per item imported?
  • Complexity — which version is easier to read, and which is easier to get wrong when someone edits it in six months? (Hint: the strong one is usually shorter.)
  • Maintainability — what has to be true for the basic version to be safe? Name what would break it.
  • What you would actually ship, and the cost of the option you rejected.

What a good answer contains.

  • A judgement, not a comparison table. The table is the work; the judgement is the answer, and it must pick one.
  • The cost of the chosen option named too. "Strong, because it is safer" is half an answer; "strong, accepting one catalogue copy per import call, which is a file-system operation an order of magnitude more expensive" is a whole one.
  • A criterion that would flip the decision, stated explicitly — for example, a call frequency that makes the copy dominate, or a caller that cannot be taught to re-read state.
  • Correct use of the vocabulary: guarantee, commit-or-rollback, noexcept swap, basic versus strong — used precisely, not as decoration.
  • Recognition that the two versions have the same interface. That is deliberate: the guarantee is a property of the implementation, and the caller must be told which one in the documentation even though the signature cannot express it.

What would make it weak. Declaring the strong version universally correct. It is not: an operation that is called in a tight loop, over a large structure, with a failure that only means "skip this item", can make copy-and-swap the dominant cost of the whole program. The interesting answers say when the strong guarantee stops being worth its copy, and what they do instead — return a status for that one item, or collect the failures and apply them in one pass.

Deciding it for yourself. Ask the question the guarantee is really about: when this fails, will the caller have already made a decision based on the state I just changed? If yes, it needs the strong guarantee. If no — if the operation is the last thing that happens before the state is re-examined anyway — basic is not a compromise, it is the correct answer.


Evaluating your own answer

Write these five lines before reading any suggested approach:

My decision:
The failure category it belongs to, and why (validate / return / throw / assert):
The cost of the option I chose:
The option I rejected, and what it would have cost:
What would change my mind:

Then check the answer against three tests:

  1. Does it name a category, or a list? A rule that classifies a failure you have not met yet is worth more than eight decisions you happened to make.
  2. Is the cost named for the option you chose? A judgement with no cost is a preference.
  3. Would it survive the second run? Lab 13's failure run is where a policy that sounded right in prose is tested against counters that do not negotiate.

Levels: [L5 Design] · [L7 Refactor] · [L8 Evaluate] — this module's exit level is 6, and Challenge 3 goes beyond it deliberately: the counterweight to a module about failure handling is knowing which failures do not deserve a mechanism.


Next: Self-assessment — nine questions, including a destructor trace and a malformed file under both guarantees.