There has always been confusion and debates on what to do with “exception” and how to use it. Now, if you see “exception handling” in context of just a programming practice, or java, or a convention, you are sure to get challenged by both your team members and yourself. During my early years, I have been in turmoil for making right decisions on it. But over the time, I have learned lessons and found the real purpose of exceptions which I am gonna share today:
Here are a few notable items:
- Exception handling is not a coding convention
- It does not belong to Java only
- It’s neither good nor bad
- It’s a part of the architectural behavior (or simply architecture) of the software you are making
Without explaining how to think of it, I would rather create a guideline through which everything will dawn on you naturally over the practice.
1. Plan for exceptions early, a lot before starting to code
If you do not plan early about the exceptions, you are sure to mix it up while coding. There are several common problems with it
- Sometimes you get “over-generalized exceptions”. So, the higher level gets inadequate information about an exception from the lower levels.
- Sometimes you get “over-bloated exceptions”. So, the higher level gets much more information than it needs.
- Sometimes you handle exceptions in the layer, and sometimes in another higher layer. You get to switch between codes in different layers a lot which is one of the most tiring habits I found in coders.
- You get to change your exceptions and handlers without notice to others! Especially, when you throw new exceptions from lower layers.
2. Define architectural layers of the software and for each layer follow a Top-Down approach in defining Exceptions
If you are not familiar with layers, please refer to some layered architecture so that you can grab the ideas coming forth. You can check these simple ones:
2.1 Communication between two different layers
So, when you have layers in your software, it’s pretty easy and wise to define a set of guideline on how they will communicate with each other. Normally, what we do every day is call a method on a lower layer object and except a return value. But, I think we can do better than that. So, to set up a guideline for communication you can think of these common issues:
- What should you return from a layer if the input is malformed, invalid, or obsolete?
- If you are not returning a valid return data, you must describe well what you are returning, returning null says null. For example, if a record is no more available, I would return a NotAvailable or AlreadyDeleted Exception. I can also return a special case object depending on the situation.
- You must provide with adequate information to the calling layer, but not too much.
So return value can be a correct value, or a special case value, or an exception.
2.2 Top-down approach
The general rule is that you start defining exceptions and handlers by a top-down approach. It is because lower level layers may not and should not focus on who is gonna use them and how. So, higher level layers define what they need to know from lower level layers. By this, you will be able to group exceptions and generalize well. The lowest level layers may produce very detailed and highly local information which is not needed at higher levels. It’s not wise to propagate unnecessary information to higher levels.
For example, the CreditCardProcessor does not need to know that MySQL Server has failed due to a disk I/O error! If you start planning at data access layer, you may end up propagating the error to the CreditCardProcessor layer. But if you think in a top-down approach, you can easily find that CreditCardProcessor just needs to know that data was not persisted in the channel it tried to. It does not need to know
- whether the database engine is MySQL
- whether there is any kind of disk
- what an i/o error is
All it needs to know that there was a problem saving the data and it can take actions from that knowledge. It must not try to take follow-up actions for MySQL errors! That’s surely another man’s job.
3. Define who is responsible for an error clearly
This is the most difficult part. Different kinds of problems happen, and we tend to implement different solutions just in the code we are currently writing. Many a time we face the “hiding exception” problem.
So, it’s better to
- list down kinds of problems that can occur in each layer.
- Define who is gonna handle the problem (taking necessary actions, logging, notifying admins, trying again, etc).
- Define who is gonna generalize the problem. Normally the one handles the problem generalizes the problem. Like all the persistence problem can be generalized by the repository classes (repository pattern by M. Fowler).
- Define who is gonna bury the problem. Someone needs to bury the problem, otherwise, this will lead the application/thread to stop incorrectly, and show the users enigmatic codes. I have found hundreds of such examples.
So, there are 3Who’s for each exception. If you don’t follow it, you will at least end up making too many logs, sending too many error reports, taking too many actions, and entangled code structure. These are very common problems. I myself logged the same exception in different layers because I did not clearly define the problems.
4. Exception Container
Exception container is a great tool for generalization and communication between different parties. Sometimes, when you are
- not sure how to propagate the necessary information
- not sure how much information a higher level may need
- not sure how many different problems a lower level may have
- not sure where should the generalization occur
- in need of adding some common behavior to errors so that it meets the guideline of incoming errors for another party. For example, a rest API may have a strict guideline that there must be an error field in return value if there is an error.
- not sure whether you need to catch RuntimeExceptions.
In these cases, you can use exception containers. Exception containers have
- some special fields which are a set of common behavior added to Exceptions
- optionally the original exception object itself.
Paradox: How much information to share across layers
Even after all the planning, I sometimes get stuck at deciding how much information should be shared across layers. By the amount of information, I not only mean the detail of the information but also the type of the information. For example, MySQL driver can throw IOException, NetworkException, ConstraintViolationException, etc. Should I return all these different exceptions from the lowest layer to a higher one? If I am confused, I tend to use an Exception Container to keep the layers compatible with each other and take necessary actions later.