Determining When to Catch Exceptions in C# or Other High-level language

Determining When to Catch Exceptions in C#

Code in a given method should generally catch exceptions only when at least one of the following actions needs to be performed:

  • Logging or gathering information about the current exception and context
  • Adding information to the exception (e.g. throwing a custom/wrapping exception)
  • Performing any cleanup
  • Attempting to recover from any exception (or a set of specific exceptions)

If none of these actions need to be performed, do not wrap the code in try-catch (although a try-finally may still be appropriate.)  Simply allow exceptions to propagate to the caller of the method (the next highest context in the call stack.)  This keeps code clean, with well-defined areas responsible for managing exceptions.

Exception Processing Standards

  • Applications and components which log or send notifications of exception details must use the methods of Monster.GlobalFramework.Diagnostics.EventLog namespace.

using GDES = Monster.GlobalFramework.Diagnostics.EventLog.Standards;
...
...
...
try
{     
// Code which may throw exception
ShoppingCartManager.DeleteShoppingCart(cart);
}
catch (Exception ex)
{
GDES.Publish(ex);
}

  • Use standard framework-defined exceptions whenever possible (e.g. System.NullReferenceException).  Tip:  Use the “wincv” tool from the .Net Command Prompt and type “Exception” to see a list of classes deriving from Exception.

  • Do not write code in such a manner that exceptions are common
    • Example: When opening a file, check for file existence first instead of relying on a FileNotFoundException to be thrown from the Open() call.

  • Throw exceptions to caller only for abnormal behavior
    • Example: In a Customer class’ GetOrders() method, do not throw an OrdersNotFoundException when a simple empty or null result will do.

  • Within a catch region, do not use throw exp, unless you are raising an exception instance you created inside the catch region.  Instead, use throw without the exception variable. 
    • Explanation:  Using throw with an exception variable causes the CLR to alter the starting point for the exception to the line the throw is on.  This means logging and stack traces for the exception will incorrectly point to the catch region, not to the originally offending code.  Using a “naked” throw does not affect the stack trace, keeping it pointing at the original line of code that introduced the exception. 





Using Framework Exceptions

Following are some usage guidelines for standard Framework exceptions:

  • Exception – Use only if no other custom or framework exception applies
  • ApplicationException - Do not use.  [See ApplicationException on page 19.]

  • ArgumentNullException- Use if an argument is null
  • ArgumentOutOfRangeException – Use if an argument's value is outside acceptable range (e.g. -1 for channelID)
  • ArgumentException - All other argument exceptions

  • NotImplementedException – Used as a temporary indicator that functionality has not yet been written.  Used when initially stubbing out methods for development.
  • InvalidOperationException - Use when calling certain functionality is not permissible given the current state of the object or system (e.g.  You might not be able to read a file that is not yet open.)
  • NotSupportedException – Indicates functionality missing by design, not because someone hasn’t done it.  Typically used with interface implementations.  This is rarely used.

General Exception Handling Approaches

There are five broad approaches for managing exceptions.  These basic approaches can be combined in many ways but should serve as building blocks for your own implementation.

  1. Catch the exception and process:

try
{
// Code which may throw an exception
}
catch(Exception exp)
{
// Completely process here
}

  1. Catch the exception, perform some processing, and rethrow it to the caller for further handling:

try
{
// Code which may throw an exception
}
catch(Exception exp)
{
// Perhaps code here to log some details

throw;  // Rethrow to caller
}

  1. Catch the exception and add more information to the exception or throw a custom exception:

try
{
// Code which may throw an exception
}
catch(Exception exp)
{
throw new CustomExampleException(“Some further description here”, exp);
}

  1. Ignore the exception and let it propagate to the caller, expecting it to be handled there (ala Chain of Responsibility design pattern.)

public int ScreamChokeAndDie(int someValue)
{
       int lameDivisor = 0;
       return someValue/lameDivisor; // This will throw a DivideByZeroException
                                     // which would fly up to the caller (which
                                     // hopefully has code to handle it.)
}

  1. Completely ignore/suppress the exception:

try
{
// Code which may throw an exception
}
catch(){/* Ignore exception */}

General Exception Handling Guidelines


  • Catch exceptions as specifically as possible with more general exception types in lower catch regions.  The order of catch regions is important because the first valid match will be used even though a more exact match may be in later catches. 

    Example:
try
{
// Code which may throw ConstraintException, DataException or other Exception
}
catch(ConstraintException exp)
{
// Handle ConstraintException, which is a subset of DataException
}
catch(DataException exp)
{
// Handle general DataException
}
catch(Exception exp)
{
// Handle general exception
}

If “catch(Exception exp)” were the first catch block, the other two would be unreachable since all exceptions ultimately derive from System.Exception.

  • Keep in mind that exceptions encountered in catch or finally blocks are treated like any other unhandled exception.  For example, if you have cleanup code in your finally block, and it throws an exception, that exception will be raised to the caller – probably not what was intended.  It is certainly possible to nest try-catch(-finally) blocks.


[For further information on handling exceptions, see Exception Management in .Net by Jones and Jezierski on MSDN.]



Creating Custom Exceptions

When none of the built-in exceptions are of sufficient detail for your code, you can implement your own custom exception.  Custom exceptions should be derived from System.Exception (or from another custom exception base) and have a name ending with “Exception”.  As with all other independent classes, your exception should be defined in a separate file named <exceptionname>.cs.

The Exception class defines three public constructors:

    public Exception();
    public Exception(string message);
    public Exception(string message, Exception innerException);

All three of these should be implemented in custom exceptions.

Custom exceptions should have the [Serializable] attribute and implement an additional (protected) constructor accepting SerializationInfo and StreamingConext (see the example below.)  This will allow your exceptions to be marshaled (i.e. for Remoting) or stored in out-of-process session state.  If you add custom fields, you need manually serialize and deserialize them (using an override of GetObjectData and the various Get methods of SerializationInfo

Finally, if your exception defines any additional properties, you should consider an override of ToString() which writes the value of those properties (in addition to the standard details!)

Serializable Custom Exception Example:
[Serializable]
public class CustomException : Exception
{
private string _additionalDetails;

       ///<summary>
/// Additional information about the exception
///</summary>
public string AdditionalDetails
       {
               get {return _additionalDetails;}
               set {_additionalDetails = value;}
       }

///<summary>
/// Default constructor
///</summary>
public CustomException() : base() {}

       ///<summary>
/// Constructor accepting only a string description
///</summary>
public CustomException(string message) : base(message) {}

///<summary>
/// Constructor accepting a description and the original exception
       /// to embed as the InnerException
       ///</summary>
public CustomException(string message, Exception innerException) : base(message, innerException) {}

// Constructor to allow proper deserialization
protected CustomException(SerializationInfo info, StreamingContext context) : base(info, context)
{
               // Restore internal state from SerializationInfo
       _additionalDetails = info.GetString(“_additionalDetails”);
}

///</summary>
/// Serializes custom exception’s state
///</summary>
public override void GetObjectData(SerializationInfo info, StreamingContext context)
{
               // Persist internal state to SerializationInfo
       info.AddValue(“_additionalDetails”, _additionalDetails, typeof(string));
               base.GetObjectData(info, context);
}

///</summary>
/// Returns a string describing the exception’s state
///</summary>
public override string ToString()
{
               return String.Format("{0} [AdditionalDetails=\"{1}\"]",
                              base.ToString(),
                              _additionalDetails);
       }
}

ApplicationException

There is a substantial amount of literature which indicates to use ApplicationException as the base type for custom exceptions and that applications should throw ApplicationExceptions in favor of Exceptions.  This has been amended by Microsoft as erroneous:

“We added ApplicationException thinking it would add value by grouping exceptions declared outside of the .NET Framework, but there is no scenario for catching ApplicationException and it only adds unnecessary depth to the hierarchy.”

“You should not define new exception classes derived from ApplicationException; use Exception instead. In addition, you should not write code that catches ApplicationException.”

Do not use catch ApplicationExceptions directly or use them as a custom exception base. 

Throwing Extended Exceptions

Alternately, instead of implementing a custom exception, you can choose to throw a new System.Exception, embedding the original exception.

Example:
try
{
// Code which may throw an exception
}
catch(Exception exp)
{
throw new Exception(“Some further description here”, exp);
}

However, you must keep in mind that the caller will only receive a generic Exception type and not the specific type of exception originally thrown (e.g. ArgumentNullException.)  The calling code is forced to check the InnerException property to strongly identify the original exception.  Deriving a custom exception allows a finer-grained handling of the exceptions by the caller using multiple catch regions specifically handing the custom exception (see General Exception Handling Guidelines above.) 


Comments

Popular posts from this blog

Cloud Computing in simple

How to Write an Effective Design Document

Bookmark