The Nova Chronicles ₇

Go beyond try/except: learn custom exceptions, chaining, and invisible traps that teach you to build resilient, recoverable systems.

The Nova Chronicles ₇

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

  1. 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 catch BaseException). Propagation climbs the stack until a compatible handler is found; otherwise you get a traceback. See Note [1]
  2. 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]
  3. Designing Custom Exceptions. Create small, sharp types like ConfigError and MissingArgumentError; catch them at program boundaries for friendly one-liners and non-zero exits, while letting unexpected bugs surface. See Note [2]
  4. Exception Chaining. Translate low-level errors to domain errors without erasing history via raise ... from e so you keep the original traceback. See Note [3]
  5. Three Tricky Errors. (a) Return in finally can suppress exceptions and reference unassigned vars. (b) Missing from e hides the root cause. (c) Overbroad except 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. [1] pathlib.Path & the core example.
    Imports Path, 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 to Path, 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 general UnicodeError.
    Control flow: else returns on success; finally is for cleanup only — avoid returns there.
    ↩ Back to code
  2. [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: A ValueError doesn’t say where the failure happened; ConfigError does. Likewise, MissingArgumentError instantly tells the operator what’s missing.
    class ConfigError(Exception): pass · class MissingArgumentError(Exception): pass
    ↩ Back to code
  3. [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. Use raise DataParseError(...) from e to preserve the original traceback and cause.
    Where: JSON via json.loads, CSV via csv.DictReader, date parsing via datetime.strptime, and similar structured inputs.
    ↩ Back to code
  4. [4] ZeroDivisionError & ValueError.
    ZeroDivisionError: Raised when dividing by zero; guard or recover (return None/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 in finally, catching Exception broadly, or dropping the original cause by omitting from 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.