Every real program runs into errors: files that do not exist, connections that drop, data in an unexpected format, users entering what they should not. The difference between fragile and robust software is not the absence of errors, but the way they are handled. Good error handling makes problems visible, understandable and contained; bad error handling hides them until they explode at the worst possible moment.
Two families of errors
It is worth distinguishing two categories from the outset:
- expected errors: they are part of normal operation. A user enters an invalid date, a configuration file is missing, an external service responds slowly. They should be anticipated and handled with sensible behaviour;
- programming errors: they are bugs. A variable is
Nonewhere it should not be, an index goes past the end of a list. They should not be "handled" by hiding them, but made obvious so they can be fixed.
Many problems come from treating the second family like the first: catching everything and carrying on as if nothing had happened.
The most common mistake: the silent catch-all
This fragment is as widespread as it is harmful:
try:
process_payment(order)
except Exception:
pass
Whatever goes wrong, be it a network problem or a typo in a variable name, is ignored. The program continues, the order may or may not be marked as paid, and no log tells the story. When someone notices, days later, reconstructing what happened is almost impossible.
The practical rule: catch only the exceptions you know how to handle, at the point where you know how to handle them.
try:
response = client.send(payment, timeout=10)
except TimeoutError:
logger.warning("Payment service timeout, order %s", order.id)
queue_for_retry(order)
Here a precise case is handled, with a concrete action and a trace in the logs. All other exceptions keep propagating.
Where to handle an exception
You do not have to handle an error where it occurs. It is often better to let it propagate up to a level that has the context to decide. A function that reads a file does not know whether the file's absence is serious or normal; the code calling it does.
An effective pattern in many applications:
- deep down, functions raise clear, specific exceptions;
- at intermediate levels, you catch only the cases you can genuinely resolve (a retry, a default value);
- at the top level (the web request handler, a service's main loop), a general handler records the error with the full traceback and returns a clean error response to the user.
This way the catch-all exists, but in a single place, and it is never silent.
Custom exceptions
For errors in your own domain it is worth defining dedicated exceptions:
class OrderError(Exception):
"""Generic error in order handling."""
class InsufficientStock(OrderError):
def __init__(self, product, requested, available):
super().__init__(
f"{product}: requested {requested}, available {available}"
)
self.product = product
The advantages are concrete: callers can catch exactly InsufficientStock without accidentally intercepting other problems, the message is informative and the attributes allow a targeted reaction, for example showing the user the available quantity.
Keeping the cause
When you turn a technical exception into a domain one, it is important not to lose the original:
try:
data = json.loads(text)
except json.JSONDecodeError as e:
raise InvalidConfiguration("Configuration file unreadable") from e
The from e syntax links the two exceptions: both will appear in the traceback, with the precise technical cause and its meaning for the application.
Cleaning up resources
Files, connections and locks must be released even when something goes wrong. The finally block always runs, but in Python the cleanest tool is the context manager:
with open("report.csv", "w") as f:
write_report(f)
The file is closed in every case, whether write_report finishes normally or raises an exception.
Comparing approaches
| Approach | When it makes sense |
|---|---|
| Let the exception propagate | The current code does not know how to recover |
| Catch and retry | Temporary errors such as network timeouts, with a limit on attempts |
| Catch and use a default value | Optional data whose absence does not compromise the result |
| Catch and transform | Translating a technical error into a domain error with from |
| General handler at the top | Recording everything and responding cleanly to the user |
In short
- Distinguish expected errors, to be handled, from programming errors, to be made visible.
- Never use a silent catch-all: catch only what you know how to handle.
- Handle the error at the level that has the context to decide, with a single general handler at the top.
- Define domain exceptions and keep the original cause with
raise ... from. - Use context managers to always release files, connections and locks.