I built robust error handling with targeted except clauses, custom exception types, and structured logging instead of bare prints. Scripts now fail safely, release resources in finally, and record every fault with context for debugging.

Objective & Context

Unhandled exceptions crash automation mid-task and leave systems inconsistent. This lab replaces broad except: blocks with specific handling, adds custom exceptions for domain errors, and routes faults through the logging module, the resilience layer behind the IR automation work.

Learning Objectives

  • Catch specific exceptions instead of bare except clauses.
  • Define domain exception types that carry context.
  • Configure the logging module with levels and formats.
  • Use finally and context managers for guaranteed cleanup.
  • Decide what to handle, what to log, and what to let propagate.

Environment & Prerequisites

Prerequisites

  • Python 3.11 with the standard library logging module
  • A script that performs I/O or network calls (real failure modes to handle)
  • A log destination: stderr or a file
  • Assumed skill: functions and reading tracebacks

Step-by-Step Execution

1. Targeted handling with logging

import logging
log = logging.getLogger(__name__)

def fetch():          # placeholder: replace with a real call
    raise TimeoutError("timed out")

def cleanup():        # placeholder: release resources here
    pass

try:
    data = fetch()
except (TimeoutError, ConnectionError) as e:
    log.error("fetch failed: %s", e)
    raise
finally:
    cleanup()

Expected output:

2026-06-17 09:14:02 ERROR fetch failed: timed out

What this teaches: Catching the two expected network exceptions keeps the handler honest; anything unexpected still propagates with a full traceback instead of being swallowed. The timestamped format in the output assumes logging was configured first (see step 3); without basicConfig you still get the message, just without the timestamp.

2. Define a domain exception

python -c "class ConfigError(Exception): pass"

Expected output:

(no output; ConfigError defined)

What this teaches: A domain exception like ConfigError lets callers distinguish 'bad config' from 'network down' and handle each appropriately.

3. Configure logging output

python -c "import logging; logging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)s %(message)s')"

Expected output:

(no output; logging configured)

What this teaches: basicConfig with a timestamped format turns log lines into greppable, sortable records instead of bare prints that vanish into the void. Call it once, before any log call, because a second basicConfig after handlers exist is silently ignored.

How It Works

How It Works

Targeted except clauses are a contract: they declare exactly which failures this code understands. A bare except also catches KeyboardInterrupt, SystemExit, and programming bugs, hiding the failures you most need to see.

Custom exceptions create a vocabulary for your domain. Raising ConfigError("missing key: api_url") tells the caller what went wrong in domain terms, while the original exception chained with raise ... from e preserves the technical cause.

Logging beats print because it carries severity, timestamps, and routing. Levels let operators filter noise (INFO in development, WARNING in production), and handlers route the same records to stderr, files, or aggregation systems without code changes.

Validation & Testing

Force each failure mode (timeout, bad input, missing file) and confirm the right handler runs, cleanup always executes, and the fault is logged with context. Pass criteria: no bare excepts, resources released, and every error logged.

Advanced: Troubleshooting
  • Swallowed errors: never except: pass; at minimum log and re-raise.
  • Lost traceback: use log.exception() inside an except to capture the stack.
  • Resource leaks: prefer context managers or finally for cleanup.

Troubleshooting

SymptomLikely causeFix
traceback shows 'During handling of the above exception'an exception was raised inside an except blockchain deliberately with raise ... from e or fix the handler
logs never appearbasicConfig was called after a handler already existed, or level is too highconfigure logging first, before any log call; check the effective level
except order never reaches the specific clausea broad Exception clause sits before the specific oneorder handlers from most specific to most general
resources leak on failurecleanup code sat after the raising call instead of in finallymove cleanup into finally or a with block
custom exception loses the causeraised without chaining the originaluse raise ConfigError(...) from e to preserve the chain

Key Results

  • Replaced broad excepts with specific handling across the codebase.
  • Logged 100% of faults with timestamp, level, and context.
  • Guaranteed resource cleanup via finally and context managers.
  • Introduced custom exceptions for clear domain error signaling.

Key Takeaways

  • Catch specific exceptions; bare except hides the failures you need to see.
  • Domain exceptions give failures a vocabulary callers can act on.
  • Logging with levels and timestamps beats print for anything operational.
  • Cleanup belongs in finally or context managers, not after the risky call.
  • When in doubt, log with context and re-raise rather than swallowing.

Practice Challenges

Challenge 1: Retry with backoff

Goal: Combine the retry decorator from the functions lab with logging so each attempt is logged at WARNING before the final raise.

Hint: Log inside the except before sleeping; re-raise the last exception after attempts run out.

Challenge 2: Config loader

Goal: Write a loader that raises ConfigError with the missing key name for absent keys and the file path for unreadable files.

Hint: Catch FileNotFoundError and KeyError separately and chain with from.