0%

Errors as Part of Program Design · practice

Preserve Useful Diagnostic Information

An ’s message is written once and read under pressure. This lesson is about making sure it still says something useful by the time anyone sees it.

Carry the values, not just the words

A message is a , and a caller that wants to do something with the details has to parse it back out. Give the exception attributes instead:

Try it

super().__init__(...) passes the message up so printing the exception still works, and the two attributes are there for code that wants the numbers. A handler can now say “you asked for 9, try 0 to 1” without unpicking a sentence.

This is worth doing exactly when a caller needs the values. If nothing will ever read error.index, the message alone is fine.

Do not lose the cause

Lesson 4 introduced raise ... from .... It matters most here, because the difference only shows up when something has gone wrong:

Try it

Without from, __cause__ is None, but Python still records the original automatically in __context__ and normally shows both exceptions with a “During handling…” connector. With from, the original is also the explicit __cause__, and the labels it as the direct cause. That explicit relationship is what tells tools and readers that the translation was intentional.

Implicit context can come from intentional translation without from, or from an accidental failure inside a handler. Explicit cause removes that ambiguity.

Two exception-chain panels. With an implicit context, raising QuizError inside a ValueError handler stores the ValueError in __context__ and the traceback says that another exception occurred during handling. With an explicit cause, raising QuizError from the caught error stores the ValueError in __cause__ and labels it as the direct cause.

Why prefer raise QuizError(...) from error over raise QuizError(...)?

The three ways information gets lost

Swallowing. except QuizError: pass deletes the event. Nothing knows it happened.

Flattening. except QuizError as error: raise QuizError("something went wrong") replaces the top-level message with a vague one. The original normally remains in the traceback as context, but callers now receive a less useful exception without the original attributes.

Logging instead of handling. Recording a failure and then continuing as though it did not happen is the same as swallowing, with a paper trail nobody reads. If you log and cannot continue safely, re-raise.

Try it

The bare raise keeps the active exception and its traceback intact. raise error raises the same again but adds the current raise site to its traceback, while raise QuizError(str(error)) creates a different exception and loses structured details unless you rebuild them deliberately.

What a good failure looks like

Put together, a diagnosable failure has four things:

Diagnostic detailWhy it helps
A specific typeso a caller can catch just this
A message naming the so a reader stops searching
Attributes carrying the valuesso code can react without parsing
A cause, when translatedso the underlying event survives

Not every error needs all four. A ValueError for an empty prompt needs the first two and nothing more. The point is that each one is a decision, and the default of “raise something and hope” makes all four badly.

The exercise builds an exception that carries its context, and a boundary that uses it.

Task

Make the failures diagnosable.

QuestionNotFound(index, available) keeps both values as attributes and passes a message up with super().__init__, so printing it reads no question at 9; this bank has 2.

InvalidResponse(prompt, response) does the same, keeping prompt and response, with a message naming both.

load_points(raw) translates int()'s failure into QuizError, keeping the original as the cause.

describe(error) turns a caught into "<TypeName>: <message>", and appends " (caused by <CauseTypeName>)" when there is a cause. It reads __cause__, not the message text.

collect(bank, answers, log) is the boundary. For each refusal it appends describe(error) to the log and carries on; it returns the score. A TypeError is not a refusal, so it is logged and then re-raised with a bare raise, leaving the original intact.