Thursday, July 27, 2006

Every client I have done consulting at, without exception (so far), has done exception handling incorrectly in .NET.

This should not be too surprising given that people in VB6 and C++ rarely did exception handling right either.

In .NET (and Java), there are just a few things you have to remember (compared to VB6/C++) to do exception handling correctly.  Yet in ~5 years of looking for these simple rules, I have not yet found them (I discovered them the hard way, by maintaining production code that did exception handling incorrectly).

Here is my attempt at documenting good (and pragmatic) exception handling practices.  (Microsoft's attempt is here: http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpguide/html/cpconBestPracticesForHandlingExceptions.asp)

These examples are specifically for VB.NET, but they basically apply in the same way to Java and C#.

How not to do it:

1)

On Error Resume Next

What you are doing:  You are ignoring *every* error and pretending it never happened and continuing execution.
Why that is bad:     Errors happen for a reason.  If you aren't even checking to see what error happened, how do you know you can safely continue to execute?  Continuing execution after an error occurred can easily lead to file or database corruption among other problems.


2)

Try
  ... code here ...
Catch
End Try

What you are doing:  You are ignoring *every* error and pretending it never happened and continuing execution.
Why that is bad:     Errors happen for a reason.  If you aren't even checking to see what error happened, how do you know you can safely continue to execute?  Continuing execution after an error occurred can easily lead to file or database corruption among other problems.


3)

Try
  ... code here ...
Catch exception As Exception
End Try

What you are doing:  You are ignoring *every* .NET Exception and pretending it never happened and continuing execution.
Why that is bad:     Errors happen for a reason.  If you aren't even checking to see what error happened, how do you know you can safely continue to execute?  Continuing execution after an error occurred can easily lead to file or database corruption among other problems.

4)

Try
  ... code here ...
Catch exception As Exception
  Throw exception
End Try

What you are doing:  You are resetting the stack trace for the error.
Why that is bad:     A normal stack trace will show the exact line the error originated from.  In this scenario, the stack trace will instead point the Throw line as the origin of the error.

--

Unless you are at the absolute top of the stack (i.e. the main method or a global exception handler), you never want to catch exceptions of type "Exception".  You should only be catching Exception classes that are below Exception in the hierarchy, and even more strongly, the leaf nodes of the Exception class hierarchy.


How to do exception handling properly:


1)

Really, try/catch is better than On Error GoTo, but this example is more for VB6 programmers who don't have try/catch.

On Error GoTo <ExceptionHandler>

You need to do all of the following in the Exception Handler:
(a) Close any open resources (database connections, etc.)
(b) Check the error/error code
(c) Check the line that the error occured on
(d) Put the error (message and stack trace with line numbers) somewhere so someone can see it (to the screen, to a log file, to a database, etc.)
(e) Take appropriate action (i.e. return an error code from the current function, exit the application, etc.)

2)

Try
  ... code here ...
Catch exception As SqlException
  Log(exception.ToString, selectStatement)
  Throw
End Try

Why is this better:
You are catching a specific exception class (SqlException) instead of something higher level like Exception.
You are logging the exception to a file, database, e-mail, etc. so a support person can log a defect for development to look at.
You are logging the database SELECT statement that was in use at the time the exception occurred.
You are rethrowing the exception as is so the stack trace will remain intact.

3)

Try
  ... code here ...
Catch innerException As SqlException
  Log(innerException.ToString)
  Throw New CustomException("SQL Statement [" & selectStatement & "]", innerException)
End Try

Why is this better:
You are catching a specific exception class (SqlException) instead of something higher level like Exception.
You are logging the exception to a file, database, e-mail, etc. so a support person can log a defect for development to look at.
You are throwing a custom exception that the calling method may be able to deal with easier/more successfully than a SqlException.
You are improving the information available in the exception.
You are including the innerException so that no information is lost on the new custom throw.

--

How do deal with third party event driven code:

If you are using badly behaved third party event driven code, it will sometimes throw away useful exception information.

For example, if you are handling/overriding events thrown by third party code, and an exception occurs in your code, that exception should go into the third party code and then make it back into your code that originally called the third party code.  Instead, the third party code may throw away the exception (because they have bad exception handling as described above).

In that case, before you leave your event handler, you need to catch every exception and log it so it doesn't get completely lost.  You should rethrow it, just in case the third party exception handling code is eventually fixed and they propagate the exception properly in the future.

Try
  ... code here ...
Catch innerException As Exception
  LogToFile("innerException [" & innerException.ToString() & "]")
  Throw New BizException("Exception in event handler for <third party package> Code", innerException)
End Try

--

A stack trace is a very important debugging tool.  If exceptions are not handled properly, stack traces get lost or mangled, making debugging much, much harder.

Here are some code samples with accompanying stack traces:

1)

Here is a fairly simple example that calls a function which causes an exception to be thrown.

This example has no try/catch block in the function, which is normal default behavior.  The try/catch block in the Main method is just so we can view the exception before the application closes and isn't necessary/useful for any other purpose.

--
Module Module1
    Sub Main()
        Try
            Dim age As Integer
            Dim ageAsString As String = "a1234"

            age = ConvertStringAgeToInteger(ageAsString)
        Catch exception As Exception
            System.Diagnostics.Debug.WriteLine(exception.ToString)
        End Try
    End Sub

    Function ConvertStringAgeToInteger(ByVal ageAsString As String) As Integer
        Dim age As Integer

        age = CInt(ageAsString)

        Return age
    End Function
End Module
--

Here is the associated stack trace.  It's pretty basic.  The DoubleType.Parse method threw a FormatException, which was then wrapped and rethrown in the IntegerType.FromString method by an InvalidCastException.  We don't have the line number in the stack trace for the Microsoft.VisualBasic component, but we do have the proper line numbers for our code.  Line 16 is the call to CInt.  Line 7 is the call to ConvertStringAgeToInteger.
--
System.InvalidCastException: Cast from string "a1234" to type 'Integer' is not valid. ---> System.FormatException: Input string was not in a correct format.
   at Microsoft.VisualBasic.CompilerServices.DoubleType.Parse(String Value, NumberFormatInfo NumberFormat)
   at Microsoft.VisualBasic.CompilerServices.DoubleType.Parse(String Value)
   at Microsoft.VisualBasic.CompilerServices.IntegerType.FromString(String Value)
   --- End of inner exception stack trace ---
   at Microsoft.VisualBasic.CompilerServices.IntegerType.FromString(String Value)
   at ExceptionHandlingSampleCode.Module1.ConvertStringAgeToInteger(String ageAsString) in C:\mmaddox\dev\Prototype\ExceptionHandlingSampleCode\Module1.vb:line 16
   at ExceptionHandlingSampleCode.Module1.Main() in C:\mmaddox\dev\Prototype\ExceptionHandlingSampleCode\Module1.vb:line 7
--

2)

We will now modify the above example to add some bad practices and see how the stack trace changes.
--
Module Module1
    Sub Main()
        Try
            Dim age As Integer
            Dim ageAsString As String = "a1234"

            age = ConvertStringAgeToInteger(ageAsString)
        Catch exception As Exception
            System.Diagnostics.Debug.WriteLine(exception.ToString)
        End Try
    End Sub

    Function ConvertStringAgeToInteger(ByVal ageAsString As String) As Integer
        Dim age As Integer

        Try
            age = CInt(ageAsString)
        Catch

        End Try
        Return age
    End Function
End Module
--

There is no output, we have completely ignored/squelched/destroyed the error and error information.  The age variable was never set as expected, but we have no way of knowing that.

3)

This is basically the same as above, but we will no longer catch non-.NET exceptions (that is the difference between "Catch" and "Catch exception As Exception".
--
Module Module1
    Sub Main()
        Try
            Dim age As Integer
            Dim ageAsString As String = "a1234"

            age = ConvertStringAgeToInteger(ageAsString)
        Catch exception As Exception
            System.Diagnostics.Debug.WriteLine(exception.ToString)
        End Try
    End Sub

    Function ConvertStringAgeToInteger(ByVal ageAsString As String) As Integer
        Dim age As Integer

        Try
            age = CInt(ageAsString)
        Catch exception As Exception

        End Try
        Return age
    End Function
End Module
--

Again, there is no output for the same reasons as above (the exception is a .NET exception, so it is caught and ignored).

4)

Now we will try to rethrow the exception incorrectly.
--
Module Module1
    Sub Main()
        Try
            Dim age As Integer
            Dim ageAsString As String = "a1234"

            age = ConvertStringAgeToInteger(ageAsString)
        Catch exception As Exception
            System.Diagnostics.Debug.WriteLine(exception.ToString)
        End Try
    End Sub

    Function ConvertStringAgeToInteger(ByVal ageAsString As String) As Integer
        Dim age As Integer

        Try
            age = CInt(ageAsString)
        Catch exception As Exception
            Throw exception
        End Try
        Return age
    End Function
End Module
--

Here is the output.  The big difference is that instead of line 16 (the call to CInt), the stack trace now shows line 19 (the Throw statement).  If the try block was 100 lines long, we would have no idea which line in the Try block caused the exception.
--
System.InvalidCastException: Cast from string "a1234" to type 'Integer' is not valid. ---> System.FormatException: Input string was not in a correct format.
   at Microsoft.VisualBasic.CompilerServices.DoubleType.Parse(String Value, NumberFormatInfo NumberFormat)
   at Microsoft.VisualBasic.CompilerServices.DoubleType.Parse(String Value)
   at Microsoft.VisualBasic.CompilerServices.IntegerType.FromString(String Value)
   --- End of inner exception stack trace ---
   at ExceptionHandlingSampleCode.Module1.ConvertStringAgeToInteger(String ageAsString) in C:\mmaddox\dev\Prototype\ExceptionHandlingSampleCode\Module1.vb:line 19
   at ExceptionHandlingSampleCode.Module1.Main() in C:\mmaddox\dev\Prototype\ExceptionHandlingSampleCode\Module1.vb:line 7
--

5)

So, now let's actually try to fix the code using a value of -1 to specify an invalid input value.
--
Module Module1
    Sub Main()
        Dim age As Integer

        Try
            Dim ageAsString As String = "a1234"

            age = ConvertStringAgeToInteger(ageAsString)
        Catch exception As Exception
            System.Diagnostics.Debug.WriteLine(exception.ToString)
        End Try

        System.Diagnostics.Debug.WriteLine("age is [" & age & "]")
    End Sub

    ' If this function returns -1, an error occured
    Function ConvertStringAgeToInteger(ByVal ageAsString As String) As Integer
        Dim age As Integer

        Try
            age = CInt(ageAsString)
        Catch exception As InvalidCastException
            age = -1
        End Try
        Return age
    End Function
End Module
--

Here is the output.  This is workable, but I'd rather see a very useful exception thrown than having to check for an error code (error codes are pretty much obsolete with .NET, unless you are doing COM Interop).  That brings us back to the very first sample code example, which I believe is more correct.  Don't catch exceptions unless you can actually do something useful with them!
--
age is [-1]
--

--

Here is an example of a case where I have found it useful to catch an exception.  In .NET 1.1, Integer.Parse will throw one of these three exceptions if the string is not parseable.  It would be quite a pain to check for these three exceptions every time we wanted to convert a string to an integer, but that is the proper way to do it (if you tried to catch a common parent exception, you might accidentally catch an OutOfMemoryException or some other exception that you should let bubble up to call stack).

An interesting note, in .NET 1.1, the Double class is the only one that has a "TryParse" method that will parse and return a boolean instead of an exception.  In .NET 2.0, they addressed this shortcoming.  The Integer class also has a TryParse method in .NET 2.0.
--
Module Module1
    Sub Main()
        Try
            If (IsValidInteger(Nothing)) Then
                System.Diagnostics.Debug.WriteLine("The Integer was valid!")
            End If
        Catch exception As Exception
            System.Diagnostics.Debug.WriteLine(exception.ToString)
        End Try
    End Sub

    Function IsValidInteger(ByVal integerAsString As String) As Boolean
        Try
            Integer.Parse(integerAsString)
        Catch argumentNullException As ArgumentNullException
            System.Diagnostics.Debug.WriteLine(argumentNullException.ToString)
            Return False
        Catch formatException As FormatException
            System.Diagnostics.Debug.WriteLine(formatException.ToString)
            Return False
        Catch overflowException As OverflowException
            System.Diagnostics.Debug.WriteLine(overflowException.ToString)
            Return False
        End Try
        Return True
    End Function
End Module
--

7/27/2006 6:48:50 AM (Central Daylight Time, UTC-05:00)  #    Disclaimer  |  Comments [0]  | 
 Wednesday, November 09, 2005

One of my personal pet programming projects at one point in time was to create some kind of screen scraper of monster.com and other job websites so I could "download" all the current job postings and do things like:

1) Rank/Prioritize the job listings by personal appeal
2) Detect job listing duplicates between monster/dice/etc.
3) Keep track of which jobs I applied for and when
4) Automate running multiple queries with multiple terms in job site specific ways so I could detect new entries in my areas of interest (something an RSS feed would normally be used for)
5) Categorize jobs by factors that were important to me - Salary, Location, Consulting vs. Full-time, How qualified I thought I was, etc.

Everyone puts that much effort into their job search, right?  ;)

I did get something of an automated process for the worst bits with tons of manual intervention required such that I could do this and it worked wonderfully.  It was never anywhere close to automated enough to share with friends though.  However, screen scrapping different web sites is not fun, rewarding work and I eventually shelved the project (and I now find most of my contract work through recruiters who I've talked to in the past and never apply for jobs from web site job listings anymore).

I still think this is very cool though:

http://www.indeed.com/jsp/apiinfo.jsp

Indeed is yet another job search engine, except with one major difference (it may not be the only job search engine with this feature, but it's the only one I know of).  Indeed publishes a Web Services API which you can query directly that seems to return job listings from Monster, Dice, etc.  This would have made my pet project above a piece of cake to implement.  This is a very good sign of things to come in the web services world!

Now if someone would just publish a web service for stock price quotes (you know it's going to happen eventually).

11/9/2005 8:13:21 AM (Central Standard Time, UTC-06:00)  #    Disclaimer  |  Comments [2]  | 
 Tuesday, September 27, 2005

Scott Hanselman is great at evaluating all the little tools developers should be usingYesterday, he brought me to PureText.

While not the bane of my existence, the "Paste Special" feature is something I use *constantly*.  Unlike hotkey maven Scott, I was doing it with mouse clicks, which is even more painful.  Windows-V to "paste special" with one click is *exactly* the most important feature I've wanted added to all Microsoft software.  Thank you Steve Miller!!!

9/27/2005 9:12:36 AM (Central Daylight Time, UTC-05:00)  #    Disclaimer  |  Comments [0]  | 
 Thursday, August 11, 2005

ASP.NET Simplicity -- When Is Too Much Simplicity a Bad Thing

This is a good post to read, plus all the comments.  Many developers have real concerns about ASP.NET 2.0 and the direction it's taken.  ASP.NET 1.X is a very good product.  I'm really not so sure about ASP.NET 2.0.

I'd like to see Microsoft step up to the plate here and instead of saying it's "working as designed" say "we are listening to your feedback and we are going to slip the release date so we can do it right".

I've thought about trying to stick with ASP.NET 1.X, but there are just so many (too many?) obstacles.  I can't throw out all of Visual Studio 2005 just because I'm not happy with the breaking changes to ASP.NET 2.0.  I really like most of the VS 2005 non-ASP.NET 2.0 related changes.  I even like some of the ASP.NET 2.0 non-breaking changes.

I'm sure there are a lot of people watching to see what Microsoft does with this one.  Backward compatibility matters.

8/11/2005 5:52:43 PM (Central Daylight Time, UTC-05:00)  #    Disclaimer  |  Comments [0]  | 
 Friday, August 05, 2005

As a Windows user, running applications from the command line is a major pain in the ass (if I preferred working from the command line, I would use Linux).  Yet, with NAnt, there is little choice but to open a command prompt.  Well, there is an open source project called NAntRunner that allows you to run NAnt from a VS.NET Add-In, but that doesn't look like what I want either as I often have to close VS.NET to allow NAnt to overwrite the DLLs VS.NET has locked (I believe this is a bug in VS.NET) and if I want to build a single solution, I just do that in the normal VS.NET way anyway.

So, I often open a command prompt, run a batch file to add nant.exe to my path (I intentionally change my path and other environment variables in an environment specific batch file so I'm sure to always grab the proper version of the applications for that particular development/build environment) and then start cd'ing around to different directory to run the minimal set of NAnt targets I need to get the job done.

So, I did a google search for how to add a right click "Open Command Prompt Here" to Windows File Explorer, so I can at least start the command prompt in an arbitrary directory quickly and easily.  There are some excellent options for this feature here.  I chose option 3 as it was simple and low risk (I can easily undo it - there is no "magic").

This makes running NAnt that much more tolerable, but I think what I really want is a Windows File Explorer Add-In to run NAnt for an arbitrary target in an arbitrary environment, running an arbitrary batch file to initialize the environment first.  It seems very doable, but just doesn't interest me enough to actually implement it.

And why does a google search for "NAnt Runner" (notice the subtle space between the words) return nothing useful?  Hmm...  MSN search would have at least gotten me where I needed to go with a couple of non-obvious clicks, but there still isn't a direct link on the first page of results.  Not that I have much search engine power, but I'm going create this link anyway in hopes that it helps other people: NAnt Runner.

8/5/2005 5:36:34 AM (Central Daylight Time, UTC-05:00)  #    Disclaimer  |  Comments [0]  | 
 Wednesday, July 27, 2005

Migrating from ASP.NET 1.1 to 2.0 can be quite a disturbing experience, at least it has been for me.  You must go through a major (mostly automated) conversion process to recompile your application for 2.0.  Most every one of your source files will be modified during conversion, so there isn't really an option of "going back" or developing a code base against both 1.1 and 2.0 in parallel (although I still hope to attempt this parallel track, it will require tremendous attention to detail and custom automation).

I wish I had read this document from Microsoft before I did the migration for the first time so I wouldn't have been so shocked and horrified at what the conversion wizard did to my code:

Common ASP.NET 2.0 Conversion Issues and Solutions

I highly recommend you at least skim it before attempting the conversion to get a vague idea of what you are in for.  Running the conversion wizard the first time for me caused major problems because of my unrealistic expectations about what I was in for.  My first 3 options for restoring a backup copy of my 1.1 project and source failed for unfortunate reasons, but luckily I had a 4th backup option that I could restore from.  Lesson learned: You can't have enough backups before running the conversion wizard (it's hard to fault Microsoft too much for my near miss, but there are a couple of things they could have done differently that would have saved me quite a bit of grief).

Microsoft has more on migrating from ASP.NET 1.1 to 2.0 here:
http://msdn.microsoft.com/asp.net/migration/upgrade/default.aspx


[via The Daily Grind]

7/27/2005 4:19:09 PM (Central Daylight Time, UTC-05:00)  #    Disclaimer  |  Comments [0]  | 
 Thursday, June 16, 2005

Gunjan Doshi: Using Visual Studio.Net to edit NAnt build files
[via Bill Arnette on WinTechOffTopic]

This very quickly makes the NAnt build script editing environment a lot more pleasant and productive.

6/16/2005 5:05:37 PM (Central Daylight Time, UTC-05:00)  #    Disclaimer  |  Comments [0]  | 

For some reason I always have to go searching for this sample code when I want it and it's never as easy to find as I want it to be.  This is a very nice starting point for creating your own custom exception classes in C#.  The translation to VB.NET is straightforward.

using System;
using System.Runtime.Serialization;

namespace YourNamespaceHere
{
    /// <summary>
    /// YourCustomException is a vanilla custom exception for this application.
    /// </summary>
    [Serializable()]
    public class YourCustomException : Exception, ISerializable
    {
        /// <summary>
       
/// Calls the default exception constructor.
       
/// </summary>
       
public YourCustomException()
        {
        }
       
/// <summary>
       
/// Calls the default exception constructor with a message parameter.
       
/// </summary>
       
/// <param name="message">The exception message.</param>
       
public YourCustomException(string message) : base(message)
        {
        }
       
/// <summary>
       
/// Calls the default exception constructor with a message and innerException parameter.
       
/// </summary>
       
/// <param name="message">The exception message.</param>
       
/// <param name="inner">The inner exception.</param>
       
public YourCustomException(string message, Exception inner): base(message, inner)
        {
        }
       
/// <summary>
       
/// Calls the default exception constructor with a serialization info and streaming context parameter.
       
/// </summary>
       
/// <param name="info">The SerializationInfo that holds the serialized object data about the exception being thrown.</param>
       
/// <param name="context">The StreamingContext that contains contextual information about the source or destination.</param>
       
public YourCustomException(SerializationInfo info, StreamingContext context) : base( info, context )
        {
        }
    }
}

Note that there is some controversy regarding whether you are supposed to inherit from Exception or ApplicationException for custom exceptions due to Microsoft giving inconsistent guidance.

This is the source of information I've giving the most authority to on this issue in my choice of inheriting from Exception:

Brad Abrams Blog

6/16/2005 5:10:04 AM (Central Daylight Time, UTC-05:00)  #    Disclaimer  |  Comments [0]  | 
 Tuesday, June 14, 2005

Trying to run NDoc (1.3.1) on a newly setup PC, I ran into this error:

From the NDoc GUI:
---
An error occured while trying to build the documentation.

Exception: NDoc.Core.DocumenterException
Unable to find the HTML Help Compiler. Please verify that the HTML Help Workshop has been installed.

Exception: NDoc.Core.DocumenterException
Unable to find the HTML Help Compiler. Please verify that the HTML Help Workshop has been installed.
---

From the NAnt NDoc Utility:
---
Error building documentation.
    Unable to find the HTML Help Compiler. Please verify that the HTML Help Workshop has been installed.
        Unable to find the HTML Help Compiler. Please verify that the HTML Help Workshop has been installed.
---

As of today, it's not well documented on Google what this error really means.

The fix was to rerun the VS.NET 2003 installer (Control Panel -> Add/Remove Programs -> Select VS.NET -> Change/Remove). Then add the "Enterprise Development Tools->Visual Studio SDKs->HTML Help 1.3 SDK" item.

My original guess was that I got the error because I hadn't installed the MSDN library yet.  Thanks to this weblog entry for pointing me in the right direction (toward the VS.NET installer):

What is "HTML Help Workshop"?

If I were Microsoft, I wouldn't consider the HTML Help compiler as part of the "Enterprise Development Tools" package and hide it in the installer.  NDoc is a best practice that every developer who is writing a component/library should use, not just enterprise developers.  IMHO, hhc.exe should be part of the core VS.NET install.

6/14/2005 4:38:37 AM (Central Daylight Time, UTC-05:00)  #    Disclaimer  |  Comments [1]  |