I was onsite at a client site today when they experienced an issue with one of their systems. No problem, jump in and figure it out: check the logs, troubleshoot, see what’s going on, and get them back online.
Well, turns out that there wasn’t much helpful information in the logs as they decided to implement a terrible exception handling “standard” — one that we’ve seen before, unfortunately. It’s one of those weird “standards” that sometimes spreads like wildfire with developers, someone tells them about an implementation that’s better for a very specific reason, and suddenly we see the propagation of NOLOCKS on every SQL query, the ternary operator replacing all if / else / switch logic, and, what we ran into this time: every method containing a try / catch block guarding all the logic and returning a boolean or some error code.
In fact, a little searching shows that this is a pretty routinely bandied about as a good thing:
- Wrap every method in try-catch
- Why should I not wrap every block in try-catch
- I need a way to wrap all my methods inside try-catch
There are a number of problems with this, one of which is what we encountered in this legacy code base: when you try to catch every exception in every method and simply return an error code, you lose valuable stack trace and error information. Moreover, the types of catch blocks that are usually strewn about when trying to prevent any error from propagating beyond each and every method tend to be painfully generic — the part of the logic that’s “handling” the exception isn’t really doing anything valuable. Quite the contrary, in fact.
Exception handling exists for a reason and that reason is to allow a graceful way to handle an exception – and better yet, it’s allows for specific code to be executed for specific exceptions. That’s right, exception handling is for handling anticipated errors, usually not generic ones. For instance, if you are making a network call, you may want to handle a situation where a timeout or read error occurs and can then handle that with some number of retries. Exception handling is for taking care of an issue that you can do something about!
Now, one caveat to this – because we usually don’t want to have a system hard fail (though sometimes there is merit to it), we will often put a single try/catch at a certain level of the code — the main processing loop, for instance, so that we can handle any uncaught exception and take care of logging in a generic way. That allows us to perhaps catch an edge case and continue processing for the vast majority of situations. That’s again a very specific use case, but what it allows us to do is be very judicious in trapping exceptions in various places in code, but fall back on a centralized handler for those exceptions we never expected. That centralization allows us to take some generic handling at a very generic place in the code and forces us to think about when we actually need to handle exceptions downstream of that.