Recommended Patterns
Some things are inevitable in life. The same is true in software development. One of those inevitable things in writing code is a system exception. The basic goal for handling exceptions in any software is to pick from 4 basic good patterns:
-
Handle
-
Wrap
-
Replace
-
Propagate
Whenever your code encounters a system error, it should take one of the above actions. Listed below are some design patterns for how to handle a .NET exception in your application.
Handle Pattern
The Handle pattern shows up most often in network transaction handling code where retries of some sort or another are normal and expected. When using this pattern, you must be diligent in avoiding blocking for long amounts of time.
private List<Widget> GetWidgets()
{
List<Widget> widgets = new List<Widget>();
try
{
//try fetching the widgets from the main server first.
widgets = DataAccess.GetWidgets(servers.Main);
}
catch (WidgetDatabaseUnavailableException offline)
{
//Let’s ‘handle’ this specific exception. In this example, we want to
//redirect to a secondary service or device.
//Make sure that we record the first failure.
Logger.Write(offline);
//try again with the alternate server cluster.
//If this attempt fails, then
//we truly have an unresolvable system failure.
//The exception will be raised to the client code.
widgets = DataAccess.GetWidgets(servers.FailOver);
}
//return our list, empty or not.
return widgets;
}
Wrap Pattern
The Wrap pattern is most commonly used to provide a more context sensible exception. The general pattern is to embed or ‘wrap’ the root exception, which may be way too verbose or technical, inside of a context specific user exception.
This allows the original exception to be accessed along with stack and trace information if it needs to be accessed. Here is how to define a ‘wrap-able’ user defined exception:
class WidgetDatabaseUnavailableException : Exception
{
public WidgetDatabaseUnavailableException(string msg,Exception x):base(msg,x)
{
}
public WidgetDatabaseUnavailableException()
: base()
{
}
}
And here is an example of a Wrap exception handling pattern:
private List<Widget> GetWidgetsWrap()
{
List<Widget> widgets = new List<Widget>();
try
{
widgets = DataAccess.GetWidgets(servers.Main);
}
catch (SqlException rootProblem)
{
//Let’s wrap this exception inside of another, more context
//specific exception.
WidgetDatabaseUnavailableException widgetException =
new WidgetDatabaseUnavailableException(
Properties.Resources.UnableToLoadWidgets,rootProblem);
//now throw our more specific exception along with detail information about
//the root cause of our problem, the SqlException problem. The complete
//stack trace is available in the inner exception to the UI code or other
//callers.
throw widgetException;
}
//return our list, empty or not.
return widgets;
}
Replace Pattern
The Replace pattern is often used to avoid exposing sensitive information, like credit card numbers and social security. It is a common pattern to replace the exception that contains sensitive information with one that has number masks or similar methods to protect customer or entity identity information from being displayed in user interfaces.
private void ChargeVisa(Charge customerCard)
{
try
{
//we are attempting to charge a customer's credit
//card. Because of Sarbanes-Oxley, we cannot display
//the customer's credit card number. If an exception
//occurs, then we will log the true exception, but
//pass a sanitized exception back to the UI.
BillingCenter.Charge(customerCard);
}
catch (CreditCardExpiredException expired)
{
//The customer's credit card did not work.
//Log the root cause of the problem (with specifics)
//to the system database.
Logger.Write(expired);
//Replace the original exception and pass a replacement
// back to the User Interface or other client code.
//The application can choose to retry the transaction and
//display sanitized customer card information to the user.
throw new BillingCenterException(customerCard.CardHolder,
customerCard.MaskedCardNumber, customerCard.Expiration, customerCard.Amount);
}
}
Propagate or ‘Bubble’
The propagate pattern targets one or more exceptions which are unconditionally thrown to the caller. However, the Propagate pattern may also be constructed to process any remaining exceptions with alternate courses of action. In the following example, we are going to retry fetching the widgets a second time as long as the database did not report ‘offline’.
private List<Widget> GetWidgetsPropagate()
{
List<Widget> widgets = new List<Widget>();
try
{
widgets = DataAccess.GetWidgets(servers.Main);
}
catch (WidgetDatabaseUnavailableException offline)
{
//Let’s ‘handle’ this specific exception. In this example,
//the widget database is offline. If the database is
//offline, there is no point in retrying. PROPOGATE this
//exception immediately upward to the caller.
throw;
}
catch (Exception allOtherExceptions)
{
//Let’s attempt to handle all other exceptions by retrying.
widgets = DataAccess.GetWidgets(servers.FailOver);
}
//return our list, empty or not.
return widgets;
}
Exception Handling Anti-patterns
In most cases, exception handling is relatively well defined. However, there are some anti-patterns that will cause red flags in code reviews. The most common errors are:
1. Suppressing Exceptions
2. Losing stack traces
3. Failure to log exceptions correctly
4. Wrapping and replacing exceptions out of context
5.
Here are some examples of each anti-pattern and a couple of good patterns
Anti-pattern: Suppress
try
{
int x;
int zero = 0;
//this will cause a DivideByZeroException
x = 50/zero;
}
catch (Exception exception)
{
//Very bad!!! I didn't do anything with the exception. The application has no
//idea that this caused a problem!
}
Anti-pattern: Suppress and Log
try
{
int x;
int zero = 0;
//this will cause a DivideByZeroException
x = 50/zero;
}
catch (Exception exception)
{
//Very bad!!!. The client code has no
//idea that this caused a problem! To make it even worse, I
//will log the exception, but the application may exhibit
//strange behaviors as the exceptions pile up during runtime
Logger.Write(exception);
}
Anti-pattern: Suppress and Return false, 0, or null
try
{
int x;
int zero = 0;
//this will cause a DivideByZeroException
x = 50/zero;
}
catch (Exception exception)
{
//don't do anything with the exception. The application has no
//idea that this caused a problem!
return false;
}
Anti-pattern: Losing Stack Trace
try
{
int x;
int zero = 0;
//this will cause a DivideByZeroException
x = 50 / zero;
}
catch(Exception exception)
{
//uh oh! We don't know what really happened. We will lose
//all stack trace information in this case.
throw new Exception("My custom exception");
}
Anti-pattern: Log and Throw
try
{
int x;
int zero = 0;
//this will cause a DivideByZeroException
x = 50 / zero;
}
catch(Exception exception)
{
//ok. We are going to log this error, but the problem is
//that the call may do the same thing – resulting in
//multiple error messages in the log of the same thing!
Logger.Write(exception);
throw ;
}
Anti-pattern: Wrap and Lose Stack
try
{
int x;
int zero = 0;
//this will cause a DivideByZeroException
x = 50 / zero;
}
catch(Exception exception)
{
//This is also quite bad. We lost the stack trace!
MyCustomException mine = new MyCustomException(“I had a bad attempt”, exception.Message);
throw mine;
}