The Quiet Terminal

← Practical Tips

Catch what you can handle, raise what you can't

2026-07-17 · 4 min read

For my first year of try/except, I wrote the same block everywhere: except:, print something encouraging, move on. It produced programs that couldn't be stopped and couldn't be debugged. The rule I eventually learned fits in the title: catch what you can handle, raise what you can't. Here's what each half means in practice.

Why bare except is harmful

while True:
    try:
        job = job_queue.get()
        run(job)
    except:
        print("job failed, moving on")

Looks sturdy. Now press Ctrl+C while it runs. KeyboardInterrupt is raised, the bare except catches it like any other exception, the loop prints "job failed, moving on" — and keeps running. You cannot stop the program. You cannot even tell a real bug from a hiccup, because every possible exception gets the same shrug.

The minimum fix: except Exception: instead of except:. KeyboardInterrupt and SystemExit don't inherit from Exception, so they pass by and Ctrl+C works again. The better fix is to catch only what you actually expect:

try:
    record = json.loads(line)
except json.JSONDecodeError:
    print("skipping malformed line:", line[:40])

That's catching what you can handle: you know this failure, and you know what to do with it. One more shape the same idea takes: keep the try block narrow. Wrapping ten lines in a single except Exception: bandage means you no longer know which line failed. Wrap the one line that can realistically fail, and the traceback does the rest.

Raise what you can't

When you can't do anything useful about an error, don't swallow it — add context and let it travel. My settings.json loader:

try:
    config = json.loads(Path("settings.json").read_text())
except json.JSONDecodeError as e:
    raise RuntimeError(f"settings.json line {e.lineno} is not valid JSON") from e

The from e is the important part: it chains the original exception underneath, so the traceback shows both — you can read it bottom up, as always, and see RuntimeError: settings.json line 2 is not valid JSON on the last line with the real parse error above it as the cause. Without from e, you'd see one and have to guess the other.

A one-line custom exception

When your script needs to signal "this is a problem with my rules", define the error you want to catch:

class MissingRule(Exception):
    """Raised when settings.json omits a rule we need."""

Then raise MissingRule("no rule for .txt files") — and callers can catch exactly that, instead of a generic Exception that catches everything including their own bugs. One class, one docstring; if the docstring says when it's raised, future you gets the context for free.

Most of my early except blocks were fear, not handling: I didn't know what could go wrong, so I caught everything, which is how you end up debugging a program that lies to you. The rule sounds strict but it's small: catch the errors you understand, raise the rest with context.