A Pattern for Better Error Handling
For several years now we have been using a simple “pattern” to make error handling a little easier. I call this IUserTargetedException pattern, after the interface that is its primary component.
The Problem
The problem that this pattern is intended to solve revolves around how to present useful (actionable) information to the end user when exceptions occur but at the same time hide technical, sensitive or otherwise inappropriate information.
Exceptional situations can be generalized into the following categories:
- A low level system error occurs about which the end user can’t really do anything to solve, except maybe try again later or call the help desk.
- A low level system error occurs about which the end user can take corrective action if properly informed.
- An exception case is detected by the application logic and a meaningful custom exception is thrown.
Some Examples
1. The Oracle Data Provider for .Net, in the event of a connection failure, has the ill manners to include all the details of the connection string (including account credentials if present) in the text of the exception. This is the kind of exception that one almost certainly does not want the end users to see. Setting aside the fact that it contain sensitive data that the user should not have access to, even if technically savvy enough to know what the error means, what can the user do? Typically there is nothing that an end user can do to fix a connectivity problem between application components.
2. On the other hand, lets say the same data provider threw an exception that resulted from a unique key constraint violation. Again the message provides some technical details about the data schema that we probably want to keep hidden. However this time, the user, properly informed, can take action to correct the problem. He can rename the record he was trying to save to give it a unique name.
There is a third scenario. Prior to processing the record, the domain logic performs a permissions check and determines that the current user is not authorized to modify this record and throws an appropriate exception. In this case the business situation may require that the user be told in detail what permissions they are lacking so that they can request them from their administrator. So the code that throws the error needs to provide a user appropriate exception message.
So we have three scenarios where exceptions occur and in each case, the end user needs to be told something. In the first case, we probably want to keep it general: “An unexpected system error has occur. Please try again later.” In the second, we want to translate the original exception message into something actionable by the user. In the latter, the exception message is actionable as such.
The problem is how to distinguish between these at the UI level. Typically this is where we put our error handling. We can put it at a lower level, but if we do, we still need that final, top level error handler in the UI; the last defense against the unexpected. So if possible, its easiest if we can simply put all our error handling there, to keep our code dry.
The Solution
To solve this problem, we create an interface as follows:
public interface IUserTargetedException { string UserTargetedMessage { get; } } |
Then in our top level error handler, we catch all exceptions and test each to see if they implement the IUserTargetedException interface. For those that do, we display the UserTargetedMessage to the user. For those that do not, we display some generic “Unexpected system error” type of message. No mater what we show the user, we will likely log all the details for diagnostic purposes.
Application to the Problem Scenarios
Lets see how we would use this approach in each of our scenarios.
In the first scenario, we have an un-anticipated system exception. In this case, the exception is not created by our code and thus will not be marked as a user targeted exception. It will be reported using the generic message.
In the second scenario, we have an anticipated system exception. This exception gets caught at a lower level, where it was expected. In the above example, this would be in our Data Access Layer. The DAL understands the business meaning of the exception and can provide a user actionable message, so it is appropriate to an error handler there. This handler wraps the original exception in an one that implements IUserTargetedExcdeption and gives it a user actionable message. Then the new exception is thrown, to be caught by the top level handler.
In the third scenario, we are throwing a custom application exception. The code throwing the exception is generally expected to know the business meaning of the exception and is responsible for providing a user actionable message. In this case, all we need to do is use a custom exception that implements IUserTargetedException.
Variations
There are several variations on this pattern.
- One simply uses IUserTargetedException as a marker interface. (An interface that has no members but simply marks a type in some way.) In this case, the Exception.Message property is expected to contain a user appropriate message.
- Alternately, in .Net and many other modern languages, marker interfaces have fallen into disfavor, with attributes being the prefered way to decorate a class. So in this case a UserTargetedExceptionAttribute can be created. This has the slight downside of being a little harder to test for in most languages.
- A final option, available in the .Net framework is to use the Exception.Data dictionary and a system wide defined key constant to define an entry in the dictionary that will either contain a string, the user appropriate message, or a boolean indicating whether Exception.Message is user appropriate.The use of Exception.Data has the advantage in that, in scenario two, we don’t need to wrap the original exception, we can simply tag it and throw it again using throw without any arguments. This allows the exception to bubble up with its original call stack in place. That can simplify any logging tasks that must be performed. On the other hand, wrapping the exception, or throwing a new one w/o wrapping can be safer in situations where you really need to make sure sensitive data does not reach the UI.
In the end, the goal is simply to provide some means of indicating if the exception contains a user appropriate message, with the default being that it does not.