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.
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.

