I often found teams convincing themselves that they don’t do flow control using exceptions, because they don’t do constructs like a
ResultException as described in Ward Cunningham’s wiki.
Things may not be that bad in most of these teams.
But I’ve seem exceptions being used in ways that are very close to flow-control, especially when the presence of said exceptions is used to trigger complex business scenarios.
You might have also seen that this indeed an oft-used method, so what is so bad about doing using exceptions this way?
1. Exceptions can increase cognitive-complexity
Rampant use of exceptions in your code-base can make it difficult to read and understand. Remember that every
catch increases cognitive complexity.
My personal preference when it comes to exceptions was always this: exceptions should be used only when something exceptional happens. In other words, throw an exception only when you need to make a fast exit from something bad that has already occurred.
For instance, if the business function being executed is one to capture a payment, and if some dependency required to fulfill that function is unavailable (like the external payment gateway is down) — throw an exception.
Another solution would be to avoid exceptions altogether, and choose another mechanism like a
However, I’ve not seen every team eager to adopt a new concept like a
Result object. Also consider that it’s not very easy to migrate existing code either.
So this got me thinking. Are there a set of guidelines that we could follow that make the code better readable while still using exceptions?
As such, I tried to prepare a list.
1. Handling Exceptions from external libraries
Handle exceptions thrown from third party libraries close to where they might be thrown. Do not handle that exception further downstream.
If we let an exception thrown by an third party propagate to all layers of your code, it increases the coupling to that library.
This can also raise subtle, uncaught errors when you switch the library to another one, if it is ever required.
2. Respect architectural boundaries
This is fairly similar to the problem above, but I mention it as a separate point because it is something that I have seen so often.
For an example, avoid handling a database specific exception thrown from the database layer in the presentation layer.
3. Handling your own exceptions
If you throw an exception in your code yourself, handle it just once.
This should be done only in one location — hopefully a dedicated
ExceptionHandler class, if you come from the Spring world.
The reason for this advice is due to the very nature of exceptions, that they can function as non-localized
As such, I would like to downgrade the problems it can raise, by having a single place to look at where an exception is handled. For a future maintainer, this is easier than having to go through a the complete execution chain to understand what happens.
This paradigm might raise some interesting problems to solve too.
We often need to catch exceptions that we have thrown ourselves, maybe update a metric or two, or a database entity, and re-throw the same or (even) a different exception so that some code downstream can handle it.
There are ways to do the above things that might involve not using exceptions in the first place, or even revising code architecture so that you satisfy the paradigm that you handle your thrown exceptions just once.
That often is not easy, but it will pay off in future when the next person tries to read and decipher what is happening.
4. Document API methods
If you are writing a method that throws an unchecked exception, please document it. Simple
JavaDoc comments will do the trick. A method that does not document the exception(s) that it throws lies about it’s contract.
Help the future maintainers of your code. If they do not have this documentation, it could be that have no idea that it
throws an exception in the non-happy path case.
What usually follows is that they either
- wrap all the code calling this method with
catchblocks, making the code unnecessarily verbose (or)
- forget to catch the exception, and might end propagating the exception to a downstream layer.
Both, bad places to be in.
These were fairly basic points that came to my mind while working on a recent project, and thinking about ways to write more maintainable and easily readable code.