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 technicality 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: 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 these situations, don’t show a message to the user. In all other situations, 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 occur. The problem is more complex when our program wants to store a file for internal use and the user does not even know about it. What would happen in that case if we show a popup which states that the user should choose another file? He would be confused, because he didn’t intend 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 question two, “What would happen if we ignore it?” Sometimes this is good practice; if a cache file could not be accessed, the software should work without the cache. Maybe it is slower, but it works, so “ignoring” this problem would be okay. Possibly, we 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 tell the user that he must change something. In case of functional errors, “ignoring” it could also be an option. Imagine you have an application that needs data from a neighbor 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 neighbor 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 the 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 advice on what he can do to solve the problem. We can tell him that he does not have 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:
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 their own exceptions at first. Typically, we have a “CustomBusinessException” and a “CustomTechnicalException” as base exceptions for other exceptions that derive from that. This often leads 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 is still a “FileNotFound” exception. A custom exception should only be introduced when there is a real benefit from 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 have 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 many 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 should you introduce one. If you just need to know that the file was not found, there is nothing wrong using the system's “FileNotFound” exception.
The next question in that context is when to throw the exception, or better, when to catch it. 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 does not make sense. When we catch it there, we're deep in the persistent layer. This layer probably has no access to the security mechanism and not at all to the GUI to show a pop-up. In my oppinion, good practice is just to let the exception bubble up and maybe catch 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 developers might say that we have to log the exception, so we must catch it as early as possible for logging needs. Thats right, 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 have to carefully 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 an inner exception. When you do the logging at the transformation point, the stack-trace is saved and we don't have to take 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 benefits.
Finally, the most import thing on exception handling is “don’t lose your head.” Good exception handling is sometimes even harder than 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 the worst case, leads to unsatisfied users annoyed by too many pop-up messages.