Error Handling Patterns That Don't Fail Silently
The difference between a script that breaks mysteriously and one you can trust is how it handles errors. Beginners either ignore errors or wrap everything in a bare except that hides real bugs. This lesson teaches the patterns professionals use: catch what you can handle, fail loudly on what you can't, and never lose the original cause.
What You'll Learn
- Why a bare
exceptis dangerous - How to catch specific exceptions and act on them
- The role of
else,finally, and raising your own errors - How to validate inputs early instead of crashing late
The Worst Pattern: Catch Everything, Do Nothing
This is the most common mistake, and it is a trap:
try:
result = risky()
except:
pass # hides EVERYTHING, including bugs you needed to see
A bare except swallows typos, keyboard interrupts, and out-of-memory errors alike. Your program limps on in a broken state and you have no idea why. Never write this.
Catch Specific Exceptions
Catch only the errors you actually know how to handle, and let the rest surface:
try:
value = data["score"]
ratio = value / total
except KeyError:
print("missing 'score' field, skipping row")
except ZeroDivisionError:
print("total was zero, using ratio of 0")
ratio = 0
Each except names the specific exception type. This makes your intent clear and lets genuinely unexpected errors crash loudly, which is what you want during development.
What "Raising" and "Propagating" Actually Mean
When something goes wrong, Python creates an exception object and stops normal execution on the spot. The exception then travels outward: out of the current function, into the function that called it, and so on up the chain of calls. At each level, Python checks whether that code sits inside a try block with a matching except. The first match wins: its handler runs, and the program continues after that try/except. If nothing matches all the way up, the program exits and prints the traceback, which is the list of calls the exception traveled through, innermost last. This traveling is called propagation, and it is a feature: an error you cannot fix here can still be caught by a caller who can, or crash loudly enough that you find it.
One more fact that explains a lot: exception types form a family tree. ZeroDivisionError, KeyError, and FileNotFoundError are all children of Exception. So except Exception: catches nearly everything. That is occasionally right at the very top of a program (log the error, keep a service alive), but far too broad anywhere else. Catch the most specific type you can actually handle.
else and finally
- The
elseblock runs only if no exception was raised. Use it to keep the risky line and the success path separate. - The
finallyblock always runs, error or not, perfect for cleanup (though a context manager is often cleaner).
try:
f = open("data.csv")
except FileNotFoundError:
print("file not found")
else:
data = f.read() # only runs if open succeeded
f.close()
finally:
print("done attempting to read")
Raise Your Own Errors Early
Do not let bad data travel deep into your code before it explodes. Validate up front and raise a clear error:
def set_learning_rate(rate):
if not 0 < rate < 1:
raise ValueError(f"learning rate must be between 0 and 1, got {rate}")
return rate
A precise error message at the boundary saves hours compared to a cryptic crash ten functions later. This "fail fast" habit is one of the highest-value things you can adopt.
Decision
An operation might fail. What do you do?
- If You can recover (default, retry, skip)
Catch the specific exception and handle it
- If The caller gave bad input
Raise a clear ValueError/TypeError early
- If You can't meaningfully handle it
Let it propagate, do not catch
A loud crash beats silent corruption
Preserve the Original Cause
When you catch an error and raise a more meaningful one, keep the original with raise ... from:
try:
config = load_config(path)
except FileNotFoundError as e:
raise RuntimeError(f"could not start: config missing at {path}") from e
The from e keeps the original traceback chained, so you see both the friendly message and the root cause. Losing the original error is a common debugging nightmare.
Try It
Run this, then call safe_divide(10, 0) and safe_divide(10, "x") to see how specific handling reacts differently.
Key Takeaways
- Never use a bare
except: pass; it hides real bugs and leaves your program broken. - Catch specific exception types and only the ones you can actually handle.
- Use
elsefor the success path andfinallyfor cleanup that must always run. - Validate inputs and raise clear errors early ("fail fast") instead of crashing deep in the code.
- Use
raise NewError(...) from originalto preserve the root cause for debugging.

