Questions to ask yourself when handling exceptions
Safety is not always marching forwards
Can I think of any combination of input where the exception can be thrown?
If there is no combination of input that you can determine that can cause the exception, then if somehow it does get thrown, your assumptions on how the program behaves have been violated. Consider letting the error bubbling up, and defering to a general error handler. In a web context, returning a 500 would be a typical example of how a general error handler would behave. Wonderfully, this is the default behaviour for many web frameworks.
Is exiting safest?
In some situations, you may even determine that exiting is safest. For example a web-based application may try to access configuration on load, and raise an exception if it can't, which if unhandled, would [by default in many languages] cause the program to exit. In a blue-green deployment pipeline, it's desirable for the application to exit on load if misconfigured, since then the deployment will fail, and the existing deployment will remain live.
Am I avoiding a 500?
500s can be helpful. If it's best to abort because your assumptions have been violated, 500s often get to the front of the queue in front of getting addressed. You're also not wasting your client's time, since the problem is on the server, not with the client as the 4XXs suggest.
Am I handling this exception this because I don’t known the range of inputs to this bit of code?
Find out what the range is. If it's hard to work out, you have just made it even harder to understand what bit of code runs under what conditions. This isn’t a "code for the future" argument, it’s making sure you know what the code does right now, for today’s behaviour [although this would also help many futures].
In some cases, it may be appropriate to iteratively refactor surrounding code to make it easier to reason about.
[This is also an argument for type safety as well, since often it's easier to be sure on ranges of inputs. However, it's not possible or realistic in many situations.]
To test the handler have I had to mock an implementation detail?
It might not be possible for the exception to be thrown in production based on input. Consider not handling the exception where it can be thrown, and instead letting it bubble up to a general handler.
Does testing the handler feel unnecessary?
You may already suspect the handler itself is unecessary. Consider removing the handler, and letting the exception bubble up to a general handler.
[Listening to instincts can be risky, but one that pushes you to have less or simpler code, I think is worthwhile to examine]
Am I subtly introducing features?
If you're catching an exception and then marching forward with a default, you have just added a feature. Make sure this is what you want: there is often a long term cost of this. Is there really a need for this feature? How have you decided the default? Often the default is chosen as the original behaviour before the change: is the best option for the current range of behaviour of the code?
Defaults may be [currently] dead code, or they may be introduce complexity. If you do want defaults, be careful of littering them throughout your code: this makes it hard to reason about their behaviour now, and hard to change later. Better is to have a specific normalization phase, where the input is converted to some standard form. If you do have checks on the data after this phase, defer to a general error handler if the data is not as it should be. However, be mindful that if you can't cause these checks to fail through any combination of input, you may have effectively dead-code.
Am I writing exception handlers for a general library that is used on one case?
If yes, you may be wasting time. It may be better to just handle the case you need to handle.
Can the case the exception handler is designed to handle, be handled earlier?
If the exception can happen though some combination of input, it might be better to refactor the code so the error case can't happen in the bowels of your algorithm/processing, but instead happens earlier. This is already hinted at in other questions, but a rough order of preference of when it's good to deal with such things:
- Compilation
- Tests
- Start of the run of the program
- Input validation
- Input normalization
- Input processing
I'm handling low-level exceptions by raising higher level/custom exceptions. Am I sure hiding the real cause is helpful?
It's often tempting to provide a "pure" level of abstraction by hiding everything the code is actually doing. However, consider what someone will need to do to investiagte this exception. If they would need to look into the lower-level cause, you have just made extra work for them unnecessarily.
As a guideline, if a layer of abstraction helps you reason about today's behaviour, feel free to add it. If it doesn't, or if you can only see how it's helpful in some future version or use of the system, be very cautious.
Am I increasing the risk of security holes?
One of the most important aspects of secure code is your understanding of what it's doing. If your assumptions on how it behaves are violated, this is evidence that the code is not as secure as you thought. Be very sure how you would like to handle this.
Keep security in mind if you are deciding to add features, which you may be doing by handling exceptions. You are increasing complexity, which makes it harder to understand the code, and again, less secure.
Am I lowering the chance of a negative consequence, by increasing the chance of a worse consequence?
Exceptions are often handled for "safety", under the assumption that letting it bubble up is "unsafe". It's often not so trivial as this: you have to reason about each case and determine what is safest in that situation. There is no perfect or always-applicable way of doing this, but the below list is in decreasing order of "safety" for many situations.
- Not having a feature
- A feature is broken by aborting
- A feature is broken by appearing to work, but it does something unexpected
- Leaking data
Be very cautious that you're not needlessly increasing the risk of items further down this list. A data leak can have extremely long term consequences, and "I didn't understand the code, but I was avoiding breaking a feature for one of our customers" will not fly at the subsequent public inquiry. I'm not even kidding.