The Nova Chronicles ₇
Go beyond try/except: learn custom exceptions, chaining, and invisible traps that teach you to build resilient, recoverable systems.
Teslaverse University — Lesson 02.1: Error Handling / Learning from Failure
“Every exception hides a lesson; every failure, a function not yet understood.”
— Teslaverse University Codex, Book of Resilience
At Teslaverse University, failure isn’t a defect in your character or your code; it’s part of your design loop. This lesson goes past basics into upper-intermediate and advanced practices: precise handlers, custom exception design, exception chaining, and spotting invisible traps. We’ll also show why this matters for our “satellite” argparse — a module that orbits the reactor core of clean error logic.
Core Example — try/except/else/finally
from pathlib import Path
def read_text(path: str) -> str:
p = Path(path)
try:
data = p.read_text(encoding="utf-8")
except FileNotFoundError as e:
raise FileNotFoundError(f"Missing file: {p}") from e
except UnicodeDecodeError as e:
raise UnicodeError(f"Can't decode '{p}' as UTF-8") from e
else:
return data
finally:
# cleanup / counters / metrics belong here — avoid returns
pass
Step-by-Step Explanation
- What an Exception Really Is. Syntax errors are caught by the parser before execution; exceptions happen during execution. Hierarchy matters (work under
Exception, don’t catchBaseException). Propagation climbs the stack until a compatible handler is found; otherwise you get a traceback. See Note [1] - The Four-Part Anatomy.
try(code that may fail) →except(narrow, specific recovery) →else(success-only path) →finally(always runs; do cleanup only). The example above demonstrates all four in practice. See Note [1] - Designing Custom Exceptions. Create small, sharp types like
ConfigErrorandMissingArgumentError; catch them at program boundaries for friendly one-liners and non-zero exits, while letting unexpected bugs surface. See Note [2] - Exception Chaining. Translate low-level errors to domain errors without erasing history via
raise ... from eso you keep the original traceback. See Note [3] - Three Tricky Errors. (a) Return in finally can suppress exceptions and reference unassigned vars. (b) Missing
from ehides the root cause. (c) Overbroadexcept Exception:silently buries failures. See Note [4]
More Examples
Custom Exceptions (Small, Sharp, Useful)
class NovaError(Exception):
"""Base for domain-specific errors."""
class ConfigError(NovaError):
pass
Exception Chaining: Preserve the Root Cause
import json
class DataParseError(NovaError): ...
Three Tricky Errors You Must Learn to See
# Tricky #1 — return in finally (plus an unassigned variable)
def divide(a, b):
try:
result = a / b
except ZeroDivisionError:
print("Cannot divide by zero.")
finally:
return result # ❌ 'result' not guaranteed; and this suppresses exceptions
Safer version:
def divide(a, b):
try:
return a / b
except ZeroDivisionError:
print("Cannot divide by zero.")
return None
Tricky #2 — Lost Cause via Missing from e
def parse_data(data):
try:
return int(data)
except ValueError as e:
raise RuntimeError("Parsing failed") # ❌ original cause lost
Fix:
def parse_data(data):
try:
return int(data)
except ValueError as e:
raise RuntimeError("Parsing failed") from e
Tricky #3 — Silent, Overbroad Except
def do_work():
try:
risky_operation()
except Exception:
pass # ❌ You didn't handle the error; you buried the witness.
Fix: Catch narrowly, communicate clearly, decide to recover or re-raise.
Common Mistakes & Anti-Patterns
- Overbroad catching:
except Exception:by default. - Swallowing errors: empty handlers that hide state corruption.
- Returning in
finally: suppresses exceptions; risks unassigned variables. - Ambiguous messages: say what failed and why with concrete context.
- Overgrown hierarchies: keep custom types minimal and meaningful.
Why This Matters for argparse (Our Satellite)
- Early, precise feedback for invalid flags or values.
- Consistent non-zero exit codes for user errors; zero on success.
- Separate user mistakes (friendly message) from program bugs (propagate during dev).
A Lesson Inside the Lesson
Professor Teslanic finishes the diagrams, then lets the room fall quiet. He types a short function — elegant, precise — and runs it. The terminal replies with a red wall: UnboundLocalError. A few students smile, most frown. He waits just long enough for discomfort to teach.
“The error wasn’t in the math,” he says. “It was in the timing. I returned before the truth existed.”
He starts to type a near-identical example. This time, several students grin before he hits Enter. He notices, pauses, and smiles back: “So I can’t trick you with the same exception twice. Good. That means the system is learning.”
(Preview) Preparing for Logging
We’ll keep logging minimal here and wire it properly in Lesson 04 — File I/O & Logging. For now, remember the boundary rule: communicate errors at program edges (CLI entry points, file/network I/O), and preserve root causes with raise ... from e. Full patterns and handlers arrive in Lesson 04.
Summary
The goal isn’t flawless code; it’s recoverable systems. Exceptions that explain themselves, messages that help users, and chains that keep the trail back to truth.
Notes & References
- [1]
pathlib.Path& the core example.
ImportsPath, a high-level, cross-platform file path class. It handles Windows/macOS/Linux quirks and gives convenient methods like.read_text(),.exists(), etc.def read_text(path: str) -> str:accepts a path string, converts toPath, and reads UTF-8 text.
Why UTF-8? Modern default; supports virtually all languages. If decoding fails (UnicodeDecodeError), we re-raise with context as a generalUnicodeError.
Control flow:elsereturns on success;finallyis for cleanup only — avoid returns there.
↩ Back to code - [2] Custom exceptions:
ConfigError&MissingArgumentError.
What they are: Domain-specific signals for configuration failures or required inputs that weren’t provided.
Where to use: Config loading/validation (.json,.env,.ini), environment variables, CLI required args.
Why: AValueErrordoesn’t say where the failure happened;ConfigErrordoes. Likewise,MissingArgumentErrorinstantly tells the operator what’s missing.class ConfigError(Exception): pass·class MissingArgumentError(Exception): pass
↩ Back to code - [3] DataParseError & exception chaining.
What: A custom error meaning “the data exists, but the structure/content is invalid” (e.g., bad JSON/CSV/date).
Why: It pinpoints the pipeline phase that failed. Useraise DataParseError(...) from eto preserve the original traceback and cause.
Where: JSON viajson.loads, CSV viacsv.DictReader, date parsing viadatetime.strptime, and similar structured inputs.
↩ Back to code - [4] ZeroDivisionError & ValueError.
ZeroDivisionError: Raised when dividing by zero; guard or recover (returnNone/inf) or re-raise with a clearer message.
ValueError: Right type, wrong value (e.g.,int("abc")). Use it to signal invalid content without a type mismatch.
Anti-patterns to avoid: returning infinally, catchingExceptionbroadly, or dropping the original cause by omittingfrom e.
↩ Back to code
Note
If you find any part of this post unclear or technically inaccurate, I would appreciate hearing from you. Improving the precision of these explanations is an ongoing process, and your feedback helps strengthen the material.