0%

Errors as Part of Program Design · practice

Translate a Low-level Error

Some errors arrive from below with a message written for a different audience.

Try it
parse_score("eight")

invalid literal for int() with base 10: 'eight'. That message is about int(). Whoever called parse_score was thinking about a score, and now has to work out what int() has to do with anything.

Catching low, raising meaningful

The fix is to catch the low-level error and raise one that speaks the caller’s language:

Try it
parse_score("eight")

Now the message is about scores, which is what the is about. This is translation: an error crossing a boundary between two levels of the program, and being restated on the way.

The pattern is worth naming because it applies far beyond int(). A function that reads a file raises “file not found”; the thing calling it wants “no saved quiz for that learner”. Same event, different vocabulary, and the caller is entitled to the second.

Keeping the cause

The version above leaves the relationship implicit, which you can inspect:

Try it

__cause__ is None, but the original int() failure is still present as implicit __context__, and Python normally includes both in the with a “During handling…” connector. That automatic context is useful, but it does not say whether the relationship was deliberate translation or merely a second failure inside the handler.

raise ... from ... makes that relationship explicit:

Try it

Now the exception records the original explicitly in __cause__: your message is for whoever reads it, and the original underneath is for whoever has to fix it. In a real traceback Python prints both, joined by “The above exception was the direct cause of the following exception”. Use from to make intentional translation unambiguous, not because implicit context would vanish otherwise.

What does raise ValueError("...") from error add over raise ValueError("...")?

Translate at a boundary, not everywhere

Translation costs a try and a handler, so it is only worth it where the vocabulary actually changes.

Worth translating. A storage error becoming a domain error. A parsing error becoming “this quiz file is malformed”. Anything where the low-level message names a tool the caller has never heard of.

Not worth translating. Passing an error along inside one layer, where nothing changes but the wording. Re-raising ValueError as ValueError with the same information adds a frame and no meaning.

There is also a version that is actively harmful:

try:
    return int(text)
except ValueError:
    return 0

That is not translation, it is concealment. A malformed score becomes a real-looking zero, and the learner’s report says they scored nothing rather than that their file is broken. If you cannot say what a means, do not invent one.

Naked re-raising

Sometimes you want to react to an error and still let it through. A bare raise inside a handler re-raises what you caught, with its traceback intact:

Try it

Recording and handling are different decisions. This lets you do the first without pretending to have done the second.

Task

Three read raw values and hand them to code that thinks in quizzes. Each should fail in the caller’s vocabulary, not the tool’s.

parse_points(text) returns the points as a whole number. On unparseable text it raises naming points and showing the offending text, with the original as the cause.

lookup_question(bank, prompt) returns the question stored under prompt. On a missing prompt it raises ValueError naming the prompt, again with the original KeyError as the cause. A missing question is a bad request, not a bad key, and the caller should not have to know the bank is a .

parse_row(parts) builds a (prompt, points) pair from a two-item like ["prompt", "3"]. A row with the wrong number of parts raises ValueError describing it. Points are parsed with parse_points, and that error passes straight through: it already speaks the right language, so re-wrapping it would add a frame and no meaning.

Finally, audited_points(text, log) appends "bad points: <text>" to the log and then lets the error continue unchanged. Use a bare raise.