User experience of an exception
At first we have to think about what an exception is. An exception occurs when a function cannot do what it was designed to do. This could be a technical reason or a functional reason. When we discover that we have just run into that case, we should carefully determine what to do next, because not every “error” in executing a function is really an error. For every exception we want to catch (or throw) we should ask:
- What went wrong and why?
- What would happen if I would just ignore it?
- What can I do to get to a defined state again?
Let’s take the classic example of saving something to a file. We could get an “AccessDenied” or “FileNotFound” exception. In both cases we could not save to file, maybe because the file is in use. If we ignore the error, our data could be lost. Now comes the hard part: Does it matter that our data is lost? In such a case we have to look at the use case. If the user wants to store some data, it is easy. The user expects that he could persist his data so we have to handle this case. To get to a defined state, we could show a popup to the user, that this file is probably in use and he should select another one. This is a technically error, but the user could do something to resolve it, this is one of exactly two cases when we should show popups to the user.
- The first case is, the user did a mistake or can solve the situation through his behaviour.
- The second case is, that the error which occurred is so serious that the program has to be closed and the user should be informed about this.
If an error occurs and it does not fit to one of the situations, don’t show a message to the user. In all other situation he couldn’t do anything and a message would be just annoying. Back to our example, we have a second case when an IO error could occure. The problem is more complex when our program want’s to store a file for internal use and the user did not even know about it. What would be if we show in that case also a popup which states that the user should choose another file? He would be confused, because he didn’t intent to store anything. Also a message that informs him, that a file could not be saved causes only questions for the user. That leads back to our question two “What would happen if we ignore it?”. Sometimes this is good practise, e.g. a cache file could not be accessed, the software should also work without cache. Maybe it is slower but it works, so “ignoring” this problem would be ok. We possibly should log the problem and give the caller a defined return value, e.g. null.
Another situation is when an error occurs, because of a functional reason. This is easier to handle, because our program normally works as it should. The problem in that case is, that business rules are violated. We have to think about how serious that is. If there is a problem with invalid input data, we should also tell the user, that he must change something. But also in case of functional errors “ignoring” it could be an option. Imagine you have an application that needs data from a neighbour system. You know that the connection is not reliable and your application is designed to handle the case, when the neighbour system is not available. From the business perspective nothing is wrong, we expected that the neighbour system could not be reached. Technically this could lead to a “SocketException” or something else. In that case we should handle the exception in a way that the application behaves normal and the user is not informed or at most through a small hint, that the other system is not reachable. Depending how often that occurs even a log entry would not be necessary, because this is a normal state although exceptions occurred.
But what to do when a vital operation fails? In that case we could probably catch the error and do some analytics of most likely reasons that might have caused the problem. In our first example we could check the file permissions, or the user level permissions. This is a difficult part, because we really have to imagine what could have happened or caused this error. But when we do a good job on this, we could give a message to the user that describes the problem and gives him an advice what he can do to solve the problem. We can tell him that he has not enough privileges to execute our software in general, or that he just has no access to a location that is needed by our software.
This is the next rule that I want to give away:
A good error message describes the loss of functionality (problem), why this occurred and what a user could do to solve it. (The third part is the most important one, but the hardest one. Most developers stop after describing the problem and leaving the user alone with it.)
The technical part of exception handling
Developers tend to introduce at first their own exceptions. Typically we have a “CustomBusinessException” and a “CustomTechnicalException” as base exception for other exception that derive from that. This leads often to the situation that in case of an “FileNotFound” exception you catch that exception, just to throw a “CustomFileNotFoundException”, derived from a “CustomIOException”, derived from the “CustomTechnicalException” and so on. But what is the benefit of that? There is no benefit of that. The “FileNotFound” exception ist still a “FileNotFound” exception. Custom exception should be only introduced when there is a real benefit of the custom exception. Imagine two different situations where the used framework throws the same exception, but from the context of our program we can distinguish between the problems that might happened. In that case there would be a real benefit to have two custom exceptions, because we can now implement two different error handling strategies. In our example we could catch the “FileNotFound” exception, check the permissions of the user and throw a “CustomNotEnoughPreviledgesException” or a “CustomLocationDoesNotExistException” instead of a simple “CustomFileNotFoundException”. Too much custom exceptions just makes the software complicated. You should use as many provided exceptions as possible and only if there is a benefit of a custom exception introduce one. If you just need to know that the file was not found, there is nothing wrong using the systems “FileNotFound” exception.
The next question in that context is, when to throw the exception or better when to catch the exception. I think the rule “throw early, catch late” is a good approach to the problem. Catching the “FileNotFound” exception directly at the “file access” statement, probably does not make sense. When we catch it there, we were deep in the persistent layer. Probably this layer has no access to the security mechanism and not at all to the GUI to show a pop-up. In my oppinion good practise is, just to let the exception bubble up and maybe catching the “FileNotFound” in an outer calling class. There we can access the security layer, check the permission and transform the exception in a “CustomNotEnoughPreviledgesException”. Some developer might now say, that we have to log the exception, so we must catch it as early as possible for logging needs. Thats rights, but when you let the exception bubble up, you have a complete stack-trace. You can log the exception, where you handle it. From the stack-trace you can easily see, what the root cause was.
Another case when you carefully have to think about exception catching and throwing is, when the exception passes a layer border. When you want to show a message for the exception, this has to be done in the presentation layer. Normally the presentation layer is not aware of a “FileNotFound” exception. It is a good idea to encapsulate exceptions when they pass a layer border, because the next layer only has to handle a few exceptions. This is the point, where our “CustomException” comes into place again. A “FileNotFound” or even better our “CustomNotEnoughPreviledgesException” should be transformed to a “CustomPersistenceException”. You could do the “transformation” by inheritance and derive from the “CustomPersistenceException”, that has the benefit that no transformation has to be done and the stack-trace reaches up through the layers. Another solution would be to throw a new “CustomPersistenceException” with the “CustomNotEnoughPreviledgesException” as inner exception. When you do the logging at the transformation point the stack-trace is saved and we must not care of it anymore. That has the benefit, that our “CustomNotEnoughPreviledgesException” can be derived from the systems “FileNotFound” exception, because actually we have a specialization of that exception. In one way or another every exception in our application could be treated as “CustomException”, so deriving from that would bring few benefit.
Finally the most import thing on exception handling is “don’t lose your head”. Good exception handling is sometimes even harder then writing the program itself. So think carefully about, how to do the exception handling and plan enough time for it. Otherwise you end up with a complicated and mostly useless exception mechanism, which in a worst case leads also to unsatisfied users annoyed by too much pop-up messages. Expect my second part of exception handling article, where I will write about message numbers, multi-language messages and the importance of the business department.