Many guides recommend us that, when we work with abstraction layers, we always wrap/chain the exceptions thrown by the internal code, with exceptions that belong to our own abstraction layer, or that at least have something to do with it. That seems reasonable and I agree with that.
However, there is something that worries me constantly. The wrapper exceptions always hide some information that could be important for the user to solve the problem appropriately. Such information is contained in the wrapped/chained exceptions, but we're supposed not to inspect them in higher layers, because that would violate the separation in layers. So, How are we supposed to inform the final user about that details? Do we show her a simple message, relative to our layer, but recommend her to check the logs if she wants more details? Even though the content of a log file may be too technical or may not be in her native language? Or are we "allowed" to inspect the wrapped exceptions if the user wants more details, to show that information in a form that is clearer that in the logs?
I'm aware that there are details that only an advanced user could understand, so she could look for them directly in the logs. But there is other type of information, like the lack of space in disk or that the connection is down, that could be understood by an average user. In those cases, it would be useful to show them something more descriptive than the typical "The operation failed". What do you think?
I'm asking this question because I believe that the information that the outermost exception can provide, is very limited. I'm referring to the exception that is on the top of the exceptions chain. Very often, I can only show a very generic message to the user, because this exception doesn't contain information that is useful enough.
With regard to this matter, some days ago I've started a new project and I've been reconsidering the design of my exceptions. After many tests, I've came to the conclusion that the best way of reusing code and of not violating the separation of layers, is to wrap the exceptions thrown by the implementation code with a single wrapper exception. One that belongs to the next higher layer. It doesn't matter if doesn't "say" much about the true error: I let top layer of the call stack to traverse the chain to know what really happened.
Like I said, for unchecked exceptions it doesn't really matter that much, because they're caused by bugs in your application that necessary will be have to fixed by a programmer, not by the end user. A generic message will do, if you don't want the application to crash catastrophically.
For checked exceptions, the code knows why they happened, because there are only very specific reasons they can happen. When you wrap those exceptions (in another checked exception), you can give the wrapper a specific and useful message. Now, it appears that this might become very messy and difficult if there are many nested wrappers, but in my experience most checked exceptions are already appropriate for the abstraction layer to bubble up without wrapping them. Usually, they will only have to be wrapped once or twice. Even then, if you want to prevent a long chain of checked exceptions, you can throw a checked exception that's appropriate for your abstraction layer, and as its underlying cause pass the cause of the wrapper that you caught. If you do that consistently, there will only be two exceptions: The wrapper that's appropriate for your abstraction layer, and the underlying cause that's at the very root of the issue:
Stephan van Hulst wrote:Yes, that's a good strategy. There's actually a pretty good way to deal with this if you're in control of most of the layers, and if you can design consistent APIs.
It comforts me to know that it's an acceptable way of dealing with exceptions. I still had some doubts.
Stephan van Hulst wrote:Like I said, for unchecked exceptions it doesn't really matter that much, because they're caused by bugs in your application that necessary will be have to fixed by a programmer, not by the end user. A generic message will do, if you don't want the application to crash catastrophically.
For checked exceptions, the code knows ...
Perfect. That's exactly how I'm doing it. I let the unchecked exceptions bubble up, whereas I wrap the checked ones with other exceptions that are appropriate to the current layer.
Stephan van Hulst wrote:Even then, if you want to prevent a long chain of checked exceptions, you can throw a checked exception that's appropriate for your abstraction layer, and as its underlying cause pass the cause of the wrapper that you caught. If you do that consistently, there will only be two exceptions: The wrapper that's appropriate for your abstraction layer, and the underlying cause that's at the very root of the issue:
Good idea! I'm going to investigate how I can adapt that to my code. It's an interesting way of easing the navigation through the exceptions chain.
Question about wrapping checked exceptions that are too specific to the implementation, but which are solvable by the final user.
I still want to have something clarified. Would you be so kind to give your opinion, please?
In your example, I've seen that you've used exceptions that belong to the same abstraction layer or that, at least, are related. But what do you do when the exception is from a lower layer, and in spite of that, the user could solve it?
I'm referring to errors that depend on external resources with which the user can interact. Basically, these are errors related to the file system (file permissions, lack of space...), the network (network is down...) or other sources of data input.
What would you do in these circumstances?
Option A: wrap these exception(s) with an unchecked exception. Then, let it bubble up, log it in the top layer, and show a generic message to the user. Maybe with a reference to the log file. That would require the user to be able to read stack traces and speak English, of course.
Option B: dissect the exception chain in the top layer, to build a human-readable message. Just as we would do with the "normal" checked exceptions. In this case, we would need to work with exceptions belonging to the implementation, which are subject to change in the future. So it's a little risky.
But the alternative is to work with the original messages from the exceptions and parse them to get details, and build localized messages. This would make the application very brittle too, because just a comma moved here or there, and the application would crash too.
For instance, when I write an application that needs to access an embedded resource (such as a configuration file inside the application JAR), the stream is allowed throw an IOException when I read a line from the file. In practice, that would only happen if the user unpacked the JAR, removed the resource, and then ran the application. The application malfunctioned because they messed with it. Reasonably, I may expect the stream to never throw an IOException. I will do something like this:
I wrapped the exception in an error, because it's never supposed to happen, because that would mean that important assumptions I make about my application are broken. At the entry point of my application, I can catch any uncaught Throwable and write them to the log (where a stack trace will be very helpful for a programmer to solve the issue).
Most of the time though, missing resources are caused by an issue that are neither the user's or the programmer's fault. They must be dealt with at the right location in the application. That means checked exceptions must be either caught and dealt with, propagated upwards or wrapped and re-thrown. Let's say your application uses some SQL driver that stores user settings in the cloud. If the connection goes down while you're trying to save the settings, you might get an SQLException. However, using an SQL driver was an implementation choice, and is absolutely not interesting to the client of your method. So, you might want to wrap it in a custom exception that you can realistically expect the client to deal with:
However, if you're saving the user settings to a file, you may realistically expect the client to deal with IOExceptions, because those can simply happen at any time when you're performing disk operations:
So, what if you catch an exception that you want to display to the user, and then carry on? You can pop up a dialog that shows the exception message, and add a "Details" button that displays a text area containing the full stack trace. In the case of the CloudException, the exception message is nice enough to show to the user directly, and if they know enough about SQL drivers, they can attempt to help themselves by clicking on the "Details" button. Here is a Swing example of how to achieve that:
About adding a "Details" button to show the stack trace, I consider it a very good idea. I'm going to try to apply it to my projects from now on.
Again, thank you fro your advices!