Exception handling best practices in ASP.NET web applications

    02. Jun 2008 at 10:56 by Janko | 15 Comments

    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 an 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 in ASP.NET web applications. This article is related to my previous articles CSS Message Boxes for different message types and Create MessageBox user control using ASP.NET and CSS since this two articles describes how to show user-friendly messages.

    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.

    Exception handling sceenshot

    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.

    Exception handling sceenshot

    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.

    ExceptionLog table

    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 a 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>

    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"); }

    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        
    // UPDATE 06.06.2008: You'll have to redirect to an error page here
    // strikethroughed code just won't work :)
    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 ASP.NET Ajax fits in this?

    As you probably know, exceptions that occur during Ajax postback are presented as a JavaScript alert, which is completely disaster for user experience. Dave Ward has an excellent article on how to handle Ajax exceptions on the client. To put it in short, the goal is to hook up on EngRequest event so that you can access EndRequestEventArgs class that provides an information about exceptions occurred. That way you can eliminate JavaScript alert and show useful information in custom message boxes like those I show you in my previous articles CSS Message Boxes and MessageBox user control using ASP.NET and CSS.

    However, if you want to be able to do some server processing when error occur you can make use of ScriptManager. You will have to define OnAsyncPostBackError handler in the definition of ScriptManager.

    <asp:ScriptManager ID="ScriptManager1" runat="server" 
    OnAsyncPostBackError="ScriptManager1_AsyncPostBackError" />

    This will allow you to have the access to the error on the server:

    protected void ScriptManager1_AsyncPostBackError(
    object sender,
    
    AsyncPostBackErrorEventArgs e) { // do whatever you need to do here
    }

    This example extends Dave's article so be sure to read it first!

    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 that occur during Ajax post back manually

    I recommend you to read the following articles:

    Currently rated 5.0 by 1 people

    • Currently 5/5 Stars.
    • 1
    • 2
    • 3
    • 4
    • 5

    Comments

    Add comment


    (Will show your Gravatar icon)  

      Country flag


    biuquote
    • Comment
    • Preview
    Loading



    Powered by BlogEngine.NET 1.4.5.0
    Copyright © Janko Jovanovic 2008