Exception handling plays an important part of application management and user experience. If implemented correctly it can make maintenance easier and bring the user experience to a higher level. If not, it can be a disaster.
How many times have you seen the error message that doesn't make any sense or at least provides some valuable information, or even better - how many times have you seen the famous error screen with exception message and a complete stack trace on yellow background? Too many times, I would say. This is why, among other things, some of my colleagues were very interested in exception handling techniques and best practices.
The goal of this article is to provide a complete overview of what exception handling is from the user perspective and the perspective of people who maintain the application, and to show the best practices of how to implement useful error handling.
1. What information should be presented to the user?
Like I mentioned before, meaningless error messages will confuse users. Not having any error message and allowing the application to stop will make them wish they never clicked on the link that pointed to your website.
Messages like "An error occurred" or "System.InvalidOperationException: The ConnectionString property has not been initialized" mean nothing to the end user. One doesn't know what has happened exactly, has the information been saved, and what should one do next.
The goal of useful exception handling is to enable users to understand what has gone wrong and to continue using your application even if an error occurred.
So, the least you have to do is show a simple error page to the user. It has to show very basic information of what happened and what the user can do next. For example, you can show the general error message and a back button that can send the user back to the previous page.
This is the minimum of what you can do. Furthermore, you can provide a user with a meaningful set of messages that will explain:
- what happened
- what will be affected
- what the user can do from there
- and any valuable support information
By doing this you are eliminating the confusion in users and allowing them to react properly. Image below shows an example of a well designed error screen.
Now let's see what else is needed.
2. Logging exceptions
In order to enable successful application maintenance you have to log exceptions. The most important thing for people who maintain web applications is the exception log. This log stores detailed information on problems that have happened.and provides developers with useful information for correcting those problems
What information should be stored in exception log?
First of all, you have to save as much information as you can get from the exception. That means date and time, exception message, exception type and stack trace.
You could, however, log more information, for example authenticated or anonymous user information. Exception Management Architecture Guide on MSDN gives the list of possible information that can be logged:
| Data | Source |
| Date and time of exception | DateTime.Now |
| Machine name | Environment.MachineName |
| Exception source | Exception.Source |
| Exception type | Type.FullName obtained from Object.GetType |
| Exception message | Exception.Message |
| Exception stack trace | Exception.StackTrace—this trace starts at the point the exception is thrown and is populated as it propagates up the call stack. |
| Call stack | Environment.StackTrace—the complete call stack. |
| Application domain name | AppDomain.FriendlyName |
| Assembly name | AssemblyName.FullName, in the System.Reflection namespace |
| Assembly version | Included in the AssemblyName.FullName |
| Thread ID | AppDomain.GetCurrentThreadId |
| Thread user | Thread.CurrentPrincipal in the System.Threading namespace |
The more information you log the easier will be for developers to determine the cause of the error and to correct it.
Where should exception log be stored?
You can log exceptions wherever you want: text file, XML file or database. I always use the database, because it allows me to track log easily and to flag exceptions that are resolved. If exception can't be stored in the database, due to a connection problem, XML file can be used.
The easiest way to do this is to have ExceptionLog table in the database and to create your own exception log provider.
This provider can be a single class that will have three methods: LogException - that will parse the exception and save all the information in the table, ResolveException - that will remove the exception from the log and GetExceptions that will return the list of unresolved exceptions.
This is an easy and simple way to trace and correct errors in a live application.
3. How to implement useful exception handling?
First, you should consider how the exceptions will be caught. If you are building an n-tier application, you will have to catch the exceptions in middle tier classes, wrap them and throw new exception with the useful information for the client.
To send useful information to the client (such as what the user can do next) you will have to create a custom exception class that inherits from ApplicationException class and add some properties. If you want to send those four bullets I described earlier, you will have to add four more properties to your class. You will also have to override the ApplicationException constructors, and GetObjectData method in order to enable serialization. This is an example of a custom exception class called MyException.
public class MyException : ApplicationException
{
#region Constructors
public MyException(string message)
: base(message)
{
}
public MyException(string message, Exception inner)
: base(message, inner)
{
}
public MyException(string message, Exception inner,
string _whatHappened, string _whatHasBeenAffected,
string _whatActionsCanUserDo, string _supportInformation)
: base(message, inner)
{
whatHappened = _whatHappened;
whatHasBeenAffected = _whatHasBeenAffected;
whatActionsCanUserDo = _whatActionsCanUserDo;
supportInformation = _supportInformation;
}
protected MyException(SerializationInfo info,
StreamingContext context)
: base(info, context)
{
whatHappened = info.GetString("whatHappened");
whatHasBeenAffected = info.GetString("whatHasBeenAffected");
whatActionsCanUserDo = info.GetString("whatActionsCanUserDo");
supportInformation = info.GetString("supportInformation");
}
#endregion
#region Methods
public override void GetObjectData(SerializationInfo info,
StreamingContext context)
{
info.AddValue("whatHasBeenAffected",
whatHasBeenAffected, typeof(String));
info.AddValue("whatActionsCanUserDo",
whatActionsCanUserDo, typeof(String));
info.AddValue("supportInformation",
supportInformation, typeof(String));
info.AddValue("whatHappened",
whatHappened, typeof(String));
base.GetObjectData(info, context);
}
#endregion
#region Properties
private string whatHappened;
private string whatHasBeenAffected;
private string whatActionsCanUserDo;
private string supportInformation;
public string WhatHappened
{
get { return whatHappened; }
set { whatHappened = value; }
}
public string WhatHasBeenAffected
{
get { return whatHasBeenAffected; }
set { whatHasBeenAffected = value; }
}
public string WhatActionsCanUserDo
{
get { return whatActionsCanUserDo; }
set { whatActionsCanUserDo = value; }
}
public string SupportInformation
{
get { return supportInformation; }
set { supportInformation = value; }
}
#endregion
}
Catching the error in the presentation layer can be done at application (global) level or at page level.
Application level
The minimum what you should do is to define a custom error page in web.config that will display default information such as error message and contact information. You can define one page for each error code, such as 404 - File not found.
<customErrors mode="RemoteOnly" defaultRedirect="~/Error.aspx">
<error statusCode="404" redirect="404.html"/>
<error statusCode="500" redirect="500.html"/>
</customErrors>
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, "Courier New", courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
The more complex way is to create a dynamic error page that will be shown each time an error occurs. This page will show all the information that is valuable to the user. You saw the example of this page earlier in this article.
To accomplish this you can use Global.asax class or an HttpModule. In both cases, the implementation will be the same. You will have to get the last error that occurred, log it (if you didn't log it in the middle tier), store it to the session and redirect to error page.
void Application_Error(object sender, EventArgs e)
{
// Get the last exception that has occurred
Exception ex = Server.GetLastError().GetBaseException();
// You can perform logging here
// Store it in the session
Session["LastException"] = ex;
// Redirect to Error page
Server.Transfer("Error.aspx");
}
.code
{
font-family: consolas, "Courier New", courier, monospace;
}
Error page will look for the exception in the session and render the information contained in it. If there isn't necessary information, error page should render default messages.
protected void Page_Load(object sender, EventArgs e)
{
if (Session != null)
{
if (Session["LastException"] != null)
{
Exception ex = (Exception)Session["LastException"];
if (ex is MyException)
{
MyException myex = (MyException)ex;
lblWhatHappened.Text = myex.WhatHappened;
lblWhatHasBeenAffected.Text = myex.WhatHasBeenAffected;
lblWhatYouCanDo.Text = myex.WhatActionsCanUserDo;
lblSupportInformation.Text = myex.SupportInformation;
}
}
}
}
Page level
If, for some reason, you want to enable users to stay on the original form if an error occurs you can handle exceptions in Page_Error event handler. You can do this if you want to perform some specific operations before displaying an error message to the user or if you want to let the user continue with work immediately. You will have to add a handler for Page.Error event manually in Page_Load.
protected void Page_Load(object sender, EventArgs e)
{
Page.Error += new System.EventHandler(Page_Error);
}
protected void Page_Error(Object sender, EventArgs e)
{
Exception ex = Server.GetLastError();
// You can perform logging here
if (ex is MyException)
{
MyException myex = (MyException)ex;
lblWhatHappened.Text = myex.WhatHappened;
lblWhatHasBeenAffected.Text = myex.WhatHasBeenAffected;
lblWhatYouCanDo.Text = myex.WhatActionsCanUserDo;
lblSupportInformation.Text = myex.SupportInformation;
}
}
The exception details should be rendered on the top of the page in a properly formatted control.
Where are Try...Catch blocks in this story?
As you saw, I dislike using Try...Catch blocks on the client. Much easier and a more practical way is to have centralized exception handling.
However, I use them intensively in the middle tier. I catch all exceptions in the top-level classes in the middle tier, wrap them to MyException, add necessary information and throw to the client. By doing this I control every exception that occurs in middle tier.
4. How does Ajax fits in this?
If you set CustomErrors section in web.config, exceptions that occur during an Ajax post back will be handled in this way. However, I strongly recommend to handle "Ajax errors" manually. You can do this by using JavaScript.
Read my previous article on this subject to find out the details.
5. Summary
To summarize, I will repeat key points in exception handling:
- You should provide the user with meaningful messages.
- You should log as much details about exception and environment as you can
- You should have a custom exception class that will be used across application
- You can display error messages either on a custom error page, or on the original page
- You should handle errors during Ajax post back manually
I recommend you to read the following articles: