Skip to main content

Chaining and Boundaries

intermediate14 min readLesson 77 of 169

raise ... from, translating low-level errors at module boundaries, and retry basics.

When layer B receives an error from layer A, callers shouldn't need to know A's internals. Translate errors at boundaries and keep the original as context with raise ... from:

class StorageError(AppError):
    """Persistence layer failed."""

def save_task(task):
    try:
        db.execute("INSERT ...", task.as_row())
    except sqlite3.Error as exc:
        raise StorageError(f"could not save task {task.id}") from exc

The from exc chain shows both tracebacks when this fails: the StorageError you raised, and the sqlite3 cause that explains it. raise X from None is the deliberate choice to hide internals when they'd leak secrets โ€” rare, and a decision, not a habit.

else and finally complete the story

try:
    value = parse(line)
except ValueError as exc:
    log.warning("bad line skipped: %s", exc)
else:
    total += value          # runs only when NO exception fired
finally:
    lines_seen += 1         # always runs โ€” cleanup

else keeps success-path code out of the try block (so its own exceptions aren't miscaught); finally is the guaranteed-cleanup hook that with formalizes.

Retries: the shape to know

Transient failures (network, locks) deserve bounded retries โ€” sleep briefly, retry a small number of times, then surface the final error. The pattern to internalize is bounded, with backoff, and the last error preserved; you'll practice the loop shape in this module's checkpoint.

Now practice

Chaining DrillsTranslate errors at boundaries and preserve the cause.2 challenges ยท ยท ~25 min