Error Handling Workflow

When to Use try / except

A short, durable guide to deciding whether a given failure deserves a try/except — and, if so, what the except block should actually do.


TL;DR

  • Catch an error only when you can recover from it. No recovery → let it raise; the traceback is the signal you want.
  • Catch the specific error you expect so the except stays narrow. Per Real Python, it's best to "catch specific exceptions to avoid masking unexpected errors." Reserve catch-alls for a single, deliberate top-level boundary.
  • Knowing the error is necessary but not sufficient — you also need a defined response.
"Try-except is intended to be used in cases where you know a particular error will happen and you know what you should do if that error happens." — Reuben

The Two-Question Test

Before writing try/except, both answers must be yes:

  1. Do I know why/when this fails? (transient network blip, missing key, bad input)
  2. Can — and should — the program continue if it does? (skip, fall back, retry, clean up)

If #2 is no, don't catch it.

Prefer checking upfront when you can. If a cheap precondition check (membership, existence) avoids the error entirely, do that (LBYL) instead of catching. Reach for try/except (EAFP) when you can't pre-check or a check would race — and note that "exception handling is fast and efficient in Python" (Real Python), so EAFP isn't a performance compromise.


The Patterns

A. When catching is right — three shapes of recovery

A1 · Optional step (nice-to-have). The failed operation wasn't required; downstream runs unchanged. Catch, log, continue.

try:
    enrich(record)            # optional
except KnownError as err:
    log.warning(err)          # downstream doesn't depend on it
# ...continue normally

A2 · Fallback required. Downstream does depend on the result, so the except must establish a recovery state — a default, an alternate branch, a flag — before continuing. This include extra clean-up that may be required if a function fails.

try:
    value = fetch()
except KnownError:
    value = DEFAULT           # establish recovery state
    mark_degraded()           # downstream adapts to it

A3 · Augment, then re-raise. You catch only to enrich the failure — capture diagnostics, attach context, fire an alert — then re-raise so it still fails loudly. The except improves observability; it does not swallow.

try:
    risky_step()
except KnownError:
    capture_diagnostics()     # screenshot, context, alert
    raise                     # still surfaces
Logging tip: inside a handler, logger.exception(...) "captures the full stack trace in the context of the except block" (Loggly) — so prefer it over log.error(str(err)).

B. When you should not write your own try/except

B1 · The library already handles it. Transient network/API failures (timeouts, 5xx, rate limits) are dealt with by configuring the client's retry/backoff — not by hand-rolling a catch. Most HTTP clients and API SDKs retry internally; don't double-wrap them.

client = Client(retries=Retry(total=3, backoff_factor=0.3,
                              status_forcelist=[429, 500, 502, 503, 504]))
client.call()                 # no hand-rolled try/except; 4xx still fails fast

Quick Reference

Situation try/except? Why / pattern
Precondition is cheaply checkable No — check first (LBYL) Avoid the error; no exception machinery
Optional step, downstream unaffected Yes — catch, log, continue A1
Result needed, but a fallback exists Yes — catch, set fallback, continue A2
Need richer diagnostics on failure Yes — catch, enrich, re-raise A3
Transient network / API error No (your own) — configure library retries B1
Module import / fully-controlled inputs No Not recoverable; traceback is the signal
Cleanup that must always run Use finally A2

References