Menu Menu
Exception handling

Exception handling

This topic is often taken for granted and treated as a not so important part of software architecture. But frequently, during the mature phases of software development cycles, this comes back and bites you. Exception handling is one of the crucial parts of software architecture and it is very important that we take it into account in the earliest phases of software development. There are some widely accepted common rules which can help us in dealing with this aspect of architecture in the best fashion. We will do a quick walkthrough.

1. Try to make an exception handling plan during the design time of software development or as soon as possible. First, define the critical errors or conditions and see which components are mandatory for the application and which are not. There are always some critical conditions like: absence of major resources, malfunctioning of some crucial underlying frameworks and similar. If these are fulfilled, the application should stop working and provide some useful user information. Otherwise, the application should be protected from crashing at all costs, even if it means that some parts will not work at full capacity or at all. This means it's acceptable that some features do not work if core functionality is fine. Now, when we have a clear picture of what's critical and what's not, we can specify exception handling behavior in more details. This is quite useful and will bring up the developers understanding of the software to a higher level and probably save a lot of time and resources.

2. Protect your application from unexpected errors. Every thread in the application should be wrapped in a global exception handler as the last line of defense or some kind of safety net. Mostly, these exception handlers will probably log exception data and maybe raise a notification. It will improve the application's stability and user experience.

//thread execution body starts here
    try
    {  
         //some code
    }
    catch (Exception ex)
    {
         //log exception data
    }
    //thread execution body ends here

3. Take security concerns into account while you are deciding which exception details you want to reveal to the user. Very detailed exception info can imply, for example, which database type is used by the application, which type of server is started in the background and so on. So, users with more skills can use this information to abuse the application. Therefore, sometimes it's better to restrict the amount of data shown to the user, either because of safety reasons, or because of user experience. Too much complicated and understandable data can deter the user from using the software. There is no general rule saying what should be shown or left in the logs or inside of message box, it varies from one application to another. But having this in mind or taking some precautions (by way of implementing some protections or restrictions) is always a plus.

4. Deliver as much of exception data as possible inside of trace files and, if allowed and safe, in the logs too. Simple exception message written in the trace file is simply not enough for a developer or technical personnel to detect the problem easily. Exception trace is always a very useful piece of data, just as the function's trace lines are (argument values and similar). If any, inner exceptions should be traced. Very often the source of the problem lies deeply in some of the lower layers of the architecture, so tracing of inner exceptions is vital. Therefore, try to make a helper function which will trace inner exceptions recursively. Also, take into account that lots of system (built-in) exceptions contain the entire list of interesting and useful properties (like HttpResponseException, SoapException and etc.), so depending on the exception type, plan which properties to trace. See the example below:

try
    {
        //some code...
    }
    catch (ArgumentException exception)
    {
        TraceException(exception);
    }

    public static void TraceException(Exception exception)
    {
        if (exception.InnerException != null)
        {
            TraceException(exception.InnerException); //go recursively through all exc. list
        }
        //Following function will trace all exception relevant data, stack trace, additional
        //properties and etc.
        Log.WriteException(exception);
    }

5. Try to put a useful human-readable log message inside of the exception handler which will be used by the end users and/or technical personnel to either resolve the problem or diagnose it.This rule shouldn't be applied for all exception handlers in the application, but for the important ones, a useful message would help a lot. Let us imagine that we have an application that depends on some windows service (it takes configuration from it during the startup), and it can't work unless that windows service is started. See the example of pseudo code below:

try
    {
    //application startup...
        ConfigurationData configuration = windowsService.GetConfiguration();    
    }
    catch (WindowsServiceEception exception)      
    {
        Log.WriteException(exception);   //logs exception relevant data
        //close the application...
    }

Instead of this, our code should look like:

try
    {
    //application startup...
        ConfigurationData configuration = windowsService.GetConfiguration();    
    }
    catch (WindowsServiceEception exception)      
    {
         Log.Write(LogLevel.Error, "Collecting configuration from the 'xx' windows service 
         failed. Please check error log and verify that the service 'xx' is started. Application will 
         be closed.");

         Log.WriteException(exception);   //logs exception relevant data
         //close the application...
    }

Here we have a clear message and even a suggestion what the user can try.

6. Use the framework's built-in exceptions, if applicable. Before you start writing your own exceptions, check whether the underlying framework already has something useful to offer. For example, .NET framework has classes like ArgumentException, ArgumentNullException, ArgumentOutOfRangeException, InvalidOperationException, InvalidCastException and so on, which are quite handy. ArgumentException is a very useful class and used often because we almost always want to protect the functions from an invalid input, for example:

public void CreateLocalUser(string userName, string password)
{
    if (string.IsNullOrWhiteSpace(userName))
    {
        throw new ArgumentException("Invalid user name supplied. It can't be null or empty.");
    }
    //some code
}

7. Create your own exception, if neither of the framework's built-in exceptions can be used. Do not throw an instance of Exception class. This is almost always a bad decision, because there is no clear distinction between this exception and others which can be raised from the same function. It doesn't make sense if you say that your function throws instances of InvalidArgumentException and Exception class, does it?

When you want to create a new custom exception, there are some general rules defining what the new exception should have. Therefore, first refer to the framework's guidelines to check what's necessary. For example, in C# and .NET, the name of the exception class should end with the word Exception and the full name should indicate the main cause why this exception is raised. Also, the custom exception should have some of the most common constructors. The exception class is a serializable class, so it's good practice to make the derived class serializable as well. This is done by adding the [Serializable] decoration at the top of the class. Again, we need to provide a special constructor for this purpose and, if necessary, to override the default serialization behavior.

[Serializable]
public class CustomException : Exception
{
    //...
    //list of custom properties...
    //...
     
    public CustomException()
    {
    }

    public CustomException(string message)
        : base(message)
    {
    }

    public CustomException(string message, Exception inner)
        : base(message, inner)
    {
    }

    protected CustomException(SerializationInfo info, StreamingContext context)
        : base(info, context)
    {
    //important for deserialization
    }
    
    //...
    // list of constructors which accept custom properties
    //...
}

To override the default serialization process of an exception, override the function GetObjectData. The purpose of this function is to prepare SerializationInfo which is used in the serialization/deserialization process. The fact is that you can live without serialization support, if you are 100% sure that you will never have to serialize exception to a file, transfer it over network stream or similar, but it is recommended practice to do it nevertheless.

8. Override the framework's built-in exceptions if it fits but you just need a few additional properties. You don't have to create a new exception from scratch instead of it. For example, if System.IO.PathToLongException class has almost everything you need, but you just want to append one additional property, you can do something like the following:

class CustomPathNotFoundException : PathTooLongException
{
    public string SpecialCode
    {
        get;
        set;
    }

    public CustomPathNotFoundException()
        : base()
    {
        //the default constructor
    }

    public CustomPathNotFoundException(string message)
        :base(message)
    {
        //the basic .NET exception constructor
    }

    public CustomPathNotFoundException(string message, Exception innerException)
        : base(message, innerException)
    {
        //the basic .NET exception constructor
    }

    public CustomPathNotFoundException(string message, string specialCode, Exception 
                   innerException)
        : base(message, innerException)
    {
        SpecialCode = specialCode;
        //custom constructor made to accept additional property
    }
}

9. Avoid leaving empty exception handlers. This is probably one of the worst things we can do, to consume an exception without any trace or log.

try
{  
     //some file system related code
}
catch (IOException ex)
{
}
//execution continues

Even if we really want just to consume it without a special treatment, we should always leave a trace or log. For example:

catch (IOException ex)
{
      Log.Write(string.Format("Task execution failed. Details: {0}.", ex));
}

10. Deep inside of components, handle only exceptions which you know how to handle. Avoid the usage of general exception handler (handler with the Exception class). This exception handler should always be used only as a safety net as described in rule 2, unless we have a very special reason to do the opposite. If you know how a server error should be handled in a manager or a helper class, catch a specific exception. For example, catch the HttpResponseException or similar. Do not use the Exception class instead of it, because naturally this will consume all other exceptions and potentially lead to unexpected application behavior. This rule might sound ridiculous, because - who could possibly do something like that? The answer is: lots of developers.

11. Do not reset an exception call stack in a handler. If you need to catch an exception and do some work but in the end you want to re-throw the same one further, use a simple throw statement without parameters, like the following:

try
{
}
catch (IOException ex)
{
    // some exception handling processing…
    throw;
}

A 'throw;' statement does not cause the stack trace to reset. The 'throw ex;' statement does. In this way you will handle the exception but also the upper level, which should eventually catch an exception, will get a stack trace and exception info which originates from the real cause and starting point.

12. If you map one exception type to another one, leave some trace about the source exception that triggered a flow. You can do it either by tracing the source exception at the spot of mapping or you can put the original exception as the inner exception of the wrapper (This rule is strongly linked with rules 4 and 5, which imply to leave as much of useful data in the exception handler as possible).

As an example, we can take the DatabaseManager class, a simple manager class which provides data and which should work either with a relational database or with a directory service as the data source. Clearly, here an upper level of application shouldn't be aware of which system is used in the background (DatabaseManager should mask this), so we need to have unique behavior regardless of which database system is used. Take a look at the following code example:

class DatabaseManager
{
    public User GetUser(string userName)
    {
        try
        {
            // configuration switch which decides which source to use
            if (config.UseRelationDatabase)    
            {
                return directoryService.GetUser(userName);
            }
            else
            {
                return relDatabaseManager.GetUser(userName);
            }
        }
        catch (DSUserNotFoundException exception)  // Directory service exception
        {
            Log.WriteException("Collecting user info from directory service failed.", exception);
            // throw a database manager specific exception, see the prefix
            throw new DBMUserNotFoundException("User cannot be found.", exception);

        }
        catch (RDBUserNotFoundException exception) // Rel. database exception
        {
            Log.WriteException("Collecting user info from rel. database failed.", exception);
            // throw a database manager specific exception, see the prefix
            throw new DBMUserNotFoundException("User cannot be found.", exception);
        }
    }
}

13. Always double check exception handling of the API you use, especially pay attention to the dispose method. In the case where the dispose method throws an exception, it could mask the real error. For example, the WCF framework throws exceptions from the dispose method. Basically, the WCF will try to close the connection gracefully inside of the dispose method even if the connection is in the faulted state. Why is this a problem? If we use a 'using' block for some piece of WCF based functionality, and if the exception occurs inside of the using block, the original exception will be masked by the exception thrown inside of the dispose method. Take a look at the following pseudo code.

using (WcfClient client = new WcfClient("initialization data"))
{
    client.getPersonsList();       // some specific WCF exception occurs during this call
}
// in here dispose is called and it will also generate exception

So, an upper layer will get the exception from the dispose method instead of the one generated inside of the 'using' statement. Therefore, investigating API and its exception handling is always a positive thing.

14. Don't use general exception handlers (handlers for unexpected errors inside of threads) for special treatment of specific exceptions. For example, if you have multiple exceptions which can occur inside of a thread and you know how to deal with each of them, avoid doing it in the general exception handler. This is often a bad design decision because at that moment, you are probably quite far away from the part which raised an error and you probably don't have enough resources available to create a specific error treatment. So, you will either spend lots of effort dragging off necessary data in the general exception handler or you will be forced to have poor exception handling in there. One more time, global exception handlers should be used exclusively as the last line of defense which prevents the application from crashing (Except in very special occasions). Therefore, if you need to handle a specific error do it as close to the part which could have raised it as you can.

15. Consider using some of the frameworks for exception handling. For example, Microsoft's Enterprise Library contains an application block specifically designed for handling exceptions and creating a simple policy for it, called: 'The Exception Handling Application Block'. It can help us in creating a consistent strategy for exception handling and also provide us with a handy way of configuring and changing exception policy in time. Please take a look at: http://msdn.microsoft.com/en-us/library/ff664698(v=pandp.50).aspx .

Latest blog posts
Coding Dojo 2019: Impressions from the Workshop
Not Just Another Conference in Copenhagen
Codegarden 2019: Top 5 Talks of the Conference
VE: conf 2019 - Zero to One
I do care about my happiness at work