What is recommended approach for associating context with exceptions thrown in UI application?
I have a UI application, that accesses a database and must also be able to perform various actions on files. Needless to say various/numerous exceptions could be thrown during the course of the application, such as:
- The database is offline.
- The file (previously scanned into a database), is not found.
- The file is locked by another user/process and cannot be accessed.
- The file's read-only attribute is set and the file cannot be modified.
- Security permissions deny access to the file (read or write).
The precise details of the error is known at the point where the exception is raised. However, sometimes you need to let the exception be caught higher up the call stack to include context with the exception, so that you can create and present a user friendly error message; e.g. a file being locked by another process could be encountered during a file copy, file move or file delete operation.
Let's say for discussion purposes we have a single method that must perform various actions on a file; it must read a file into memory, modify the data and write the data back out as in the below example:
private void ProcessFile(string fileName)
{
try
{
string fileData = ReadData(fileName);
string modifiedData = ModifyData(fileData);
WriteData(fileName, modifiedData);
}
catch (UnauthorizedAccessException ex)
{
// The caller does not have the required permission.
}
catch (PathTooLongException ex)
{
// The specified path, file name, or both exceed the system-defined maximum length.
// For example, on Windows-based platforms, paths must be less than 248 characters
// and file names must b开发者_开发问答e less than 260 characters.
}
catch (ArgumentException ex)
{
// One or more paths are zero-length strings, contain only white space, or contain
// one or more invalid characters as defined by InvalidPathChars.
// Or path is prefixed with, or contains only a colon character (:).
}
catch (NotSupportedException ex)
{
// File name is in an invalid format.
// E.g. path contains a colon character (:) that is not part of a drive label ("C:\").
}
catch (DirectoryNotFoundException ex)
{
// The path specified is invalid. For example, it is on an unmapped drive.
}
catch (FileNotFoundException ex)
{
// File was not found
}
catch (IOException ex)
{
// Various other IO errors, including network name is not known.
}
catch (Exception ex)
{
// Catch all for unknown/unexpected exceptions
}
}
When logging and presenting error messages to the user we want to be as descriptive as possible as to what went wrong along with any possible recommendations that could lead to a resolution. If a file is locked, we should be able to inform the user of such, so that he could retry later when the file is released.
In the above example, with all the exception catch clauses, we would still not know which action (context) lead to the exception. Did the exception occur while opening and reading the file, when modifying the data or when writing the changes back out to the file system?
One approach would be to move the try/catch block to within each of the these "action" methods. This would mean copying/repeating the same exception handling logic into all three methods. And of course to avoid repeating the same logic in multiple methods we could encapsulate the exception handling into another common method, which would call for catching the generic System.Exception and passing it on.
Another approach would be to add an "enum" or other means of defining the context, so that we know where the exception occurred as follows:
public enum ActionPerformed
{
Unknown,
ReadData,
ModifyData,
WriteData,
...
}
private void ProcessFile(string fileName)
{
ActionPerformed action;
try
{
action = ActionPerformed.ReadData;
string fileData = ReadData(fileName);
action = ActionPerformed.ModifyData;
string modifiedData = ModifyData(fileData);
action = ActionPerformed.WriteData;
WriteData(fileName, modifiedData);
}
catch (...)
{
...
}
}
Now, within each catch clause, we would know the context of the action being performed when the exception was raised.
Is there a recommended approach in addressing this problem of identifying context related to an exception? The answer to this problem maybe subjective, but if there is a design pattern or recommended approach to this, I would like to follow it.
When you create the exception, set it's Message property to something descriptive before throwing it. Then higher up you can just display this Message to the user.
We normally log the exception (with either log4net or nlog) and then throw a custom exception with a friendly message that the user can understand.
My opinion is that the MS approach to localize the message property of exceptions is all wrong. Since there are language packs for the .NET framework you get from a Chinese installation cryptic (e.g. Mandarin) messages back. How am I supposed to debug this as a developer who is not a native speaker of the deployed product language? I would reserve the exception message property for the technical developer oriented message text and add a user message in its Data property.
Every layer of your application can add a user message from its own perspective to the current thrown exception. If you assume the that the first exception knows exactly what did go wrong and how it could be fixed you should display the first added user message to the user. All architectural layers above will have less context and knowledge about the specific error from a lower layer. This will result in less helpful error messages for the user. It is therefore best to create the user message in the layer where you have still enough context to be able to tell the user what did go wrong and if and how it can be fixed.
To illustrate the point assume a software where you have a login form, a web service, a backend and a database where the user credentials are stored. Where would you create the user message when a database problem is detected?
- Login Form
- Web Service
- Backend
Database Access Layer
- IResult res = WebService.LoginUser("user", "pwd");
- IResult res = RemoteObject.LoginUser("user","pwd");
- string pwd = QueryPasswordForUser("user");
- User user = NHibernate.Session.Get("user"); -> SQLException is thrown
The Database throws a SQLException because the db it is in maintainance mode.
In this case the backend (3) does still have enough context to deal with DB problems but it does also know that a user tried to log in.
The UI will get via the web service a different the exception object because type identity cannot be preserved accross AppDomain/Process boundaries. The deeper reason is that the remote client does not have NHibernate and SQL server installed which makes it impossible to transfer the exception stack via serialization.
You have to convert the exception stack into a more generic exception which is part of the web service data contract which results in information loss at the Web Service boundary.
If you try at the highest level, the UI, try to map all possible system errors to a meaningful user message you bind your UI logic to the inner workings in your backends. This is not only a bad practice it is also hard to do because you will be missing context needed for useful user messages.
catch(SqlException ex)
{
if( ex.ErrorCode == DB.IsInMaintananceMode )
Display("Database ??? on server ??? is beeing maintained. Please wait a little longer or contact your administrator for server ????");
....
Due to the web service boundary it will be in reality more something like
catch(Exception ex)
{
Excepton first = GetFirstException(ex);
RemoteExcepton rex = first as RemoteExcepton;
if( rex.OriginalType == "SQLException" )
{
if( rex.Properties["Data"] == "DB.IsMaintainanceMode" )
{
Display("Database ??? on server ??? is beeing maintained. Please wait a little longer or contact your administrator for server ????");
Since the exception will be wrapped by other exceptions from other layers you are coding in the UI layer against the internals of your backend.
On the other hand if you do it at the backend layer you know what your host name is, you know which database you did try to access. Things become much easier when you do it at the right level.
catch(SQLException ex)
{
ex.Data["UserMessage"] = MapSqlErrorToString(ex.ErrorCode, CurrentHostName, Session.Database)'
throw;
}
As a general rule you should be adding your user messages to the exception in the deepest layer where you still know what the user tried to do.
Yours, Alois Kraus
You should throw different exception types if possible from each method that can. For example, your ModifyData method could internally catch shared exception types and rethrow them if you are worried about .NET exception collision.
You could create your own exception class and throw it back up the user from your catch block and put the message into your new exception class.
catch (NotSupportedException ex)
{
YourCustomExceptionClass exception = new YourCustomExceptionClass(ex.message);
throw exception;
}
You can save as much info as you want into your exception class and that way the user has all the information and only the information that you want them to have.
EDIT:
In fact, you could make an Exception member in your Custom Exception class and do this.
catch (NotSupportedException ex)
{
YourCustomExceptionClass exception = new YourCustomExceptionClass(ex.message);
exception.yourExceptionMemberofTypeException = ex;
throw exception;
}
This way, you can give the user a nice message, but also give them the underlying inner exception. .NET does this all the time with InnerException.
精彩评论