Exception handling seems easy. But, done poorly, it can cause problems of its own or worse, it can cause problems that hide other problems.
Eating Exceptions
The worst anti-pattern is eating exceptions. This is when an exception is caught, nothing (or minimal logging) is done to handle the exception, and processing continues.
try { ... Do Stuff ... } catch { Debug.WriteLine("Oh no, an exception occurred.") }
This pattern causes some of the hardest to find bugs. Especially when the try block calls code in other classes.
Why are these bugs so nasty? The failure occurs leaving an object (or objects) in an invalid state. The failure is ignored and processing continues assuming everything is good. This can lead to corrupted data, and other, hard to reproduce exceptions.
Finding these exceptions is onerous; usually requiring some luck as well as skill.
What’s the fix for this pattern? 99+% of the time , the best fix is to remove the try/catch block. Let the exception be handled by the application-level handlers; let the stop once an object is in an invalid state.
One of the first things I do when I start on a new codebase is to remove try/catch blocks that are not:
1. Application-level exception handlers
2. Handling an exception
This sometimes causes some angst from the other developers. But it will also slowly (or, sometimes, not so slowly) expose those hard-to-find bugs that have been lurking so that they can be fixed.
Losing Exception Information
Another common problem is losing track of some of the critical information about the exception, most commonly, the stack trace.
Let’s assume we are doing some database update code, and we need to handle exceptions to issue a rollback.
var transaction = new Transaction(connection); try { ... update the database ... transaction.Commit(); } catch (Exception e) { transaction.Rollback(); throw new Exception($"Issued rollback {e.Message}"); }
In this example, the stack trace is lost. Does that matter?
It can when you are trying to find and fix the bug. Assume you are using Entity Framework and SQL Server. One common exception is an EntityException (or a descendant) with the message:
String or binary data would be truncated. The statement has been terminated.
Finding the problem behind this error is a challenge in the best of circumstances. Finding it without knowing which update caused the problem is that much more difficult.
Fortunately, fixing this problem is easy. Instead of appending the original exception message, send the entire exception
var transaction = new Transaction(connection); try { ... update the database ... transaction.Commit(); } catch (Exception e) { transaction.Rollback(); throw new Exception($"Issued rollback", e); }
Now, assuming the application level exception handler provides stack traces, finding the statement that caused the problem is easy.
And, like the previous anti-pattern, I search for references to the Message property of any exception when working on a new codebase. And I refactor to pass the entire exception because the information being lost is too important.