Catch-all Exception Handling
Before you begin: Functions, exceptions, and try/catch or try/except.
Catch-all exception handling catches a broad class of failures. The smell appears when code treats all of those failures as something it knows how to recover from—often by returning a default that looks like success. 1
Suppose an inventory display reads a count as text. Zero items, malformed text, and an unavailable storage device are three different situations. Returning zero for all three makes the display confidently wrong.
Original version: every problem becomes zero
The examples receive a function that supplies text, allowing us to show the error handling without a real file or server.
public static class Counts
{
public static int Read(Func<string> readText)
{
try
{
return int.Parse(readText());
}
catch (Exception)
{
return 0;
}
}
}The catch covers both retrieving the text and parsing it. An IOException from readText becomes the same zero as a genuine "0" result.
def read_count(read_text):
try:
return int(read_text())
except Exception:
return 0The handler covers both retrieving the text and parsing it. An OSError from read_text becomes the same zero as a genuine "0" result. Exception does not include every possible Python exception, but it is still much broader than the expected parsing failure.
Revised version: handle only the condition you understand
Here the chosen contract is: return a count for valid integer text, return an absent value for malformed text, and let retrieval failures reach a caller that can report them.
public static class Counts
{
public static int? Read(Func<string> readText)
{
string text = readText();
if (int.TryParse(text, out int count))
{
return count;
}
return null;
}
}int? count = Counts.Read(() => "not a number");
Console.WriteLine(count.HasValue ? count.Value.ToString() : "Invalid count");readText is outside any catch. TryParse handles the expected conversion failure without hiding failures in the text supplier. Values outside C#‘s integer range also produce an absent result.
def read_count(read_text):
text = read_text()
try:
return int(text)
except ValueError:
return Nonecount = read_count(lambda: "not a number")
print("Invalid count" if count is None else count)read_text is outside the handler. Only a ValueError from integer conversion becomes None. A broken supplier returning an inappropriate object is not silently treated as valid text. Python integers do not have the same fixed range as C# integers.
Broad catches have legitimate homes
A request boundary or worker loop may need a broad handler so one failed operation does not terminate the whole process. That handler should record the failure, choose an honest failure result, and preserve cancellation and cleanup requirements. It should not report that the failed operation succeeded.
This revision changes the error contract intentionally: callers must handle an absent count, and retrieval failures can now propagate. It is a correction, not a behavior-preserving refactoring.
Ask of every catch: which failure do we expect, what recovery are we performing, and what information would a caller lose? Catching an exception is not itself recovery.
Sources
- Microsoft — Best practices for exceptions, on recovery, specific handling, and exception preservation.
- Python — Errors and exceptions, on matching handlers and propagating unhandled exceptions.