Click here to Skip to main content
15,883,772 members
Articles / Programming Languages / Visual Basic
Article

Custom Exceptions in Custom Components

Rate me:
Please Sign up or sign in to vote.
4.21/5 (14 votes)
25 Sep 200711 min read 45.4K   41   5
This article is about when to throw an exception, why take some effort and define a custom exception class, how to provide more information to the developers that consume your component, and, finally, how to unit test your exceptions.

Overview

Most articles on exceptions cover only the ideas and techniques of exception handling. The reason is simple: most of us write end-user software and so throwing an exception makes little sense. However, if you are writing a custom component, the only way to notify the user of your component when something goes wrong is to throw an exception.

In this article, I'm going to give you some ideas as to when throw an exception, why take the effort to define a custom exception class, how to provide more information to the developers that consume your component and, finally, how to unit test your exceptions. I'll try to avoid labels like "good" or "bad," so that you can see what's best for yourself.

Introduction

Throwing an exception is usually done to tell some other part of your program that something's gone wrong. Doing this in the GUI layer rarely makes sense, since you have other means of handling this situation: just show a message, do some logging, try to recover or just quit. Doing it in the business layer is more appropriate: you don't want to show any visual elements from here, and you definitely don't want to close the application, so if you can't recover, at least tell the application that something went wrong. In simple cases, you can do just fine with a general Exception class, but if you want to handle different "wrongs" in a different manner or, for example, log more information about this "wrong," you have to invent a custom exception and put all of the relevant information into that class.

When you are building a custom component or when you work in a team, custom exceptions become a necessity. Let's see it in more detail.

Custom Exceptions

Typically you want to throw custom exceptions in two different situations. The first is when some other component calls your method and supplies an invalid argument. Sometimes throwing an InvalidArgumentException is fine, but if you want to help the developer who uses your component, you might want to throw a custom exception here. The second situation is when an underlying component throws an exception. We'll discuss these one-by-one.

Invalid Input

Suppose you provide a component that just divides x by y. You want to check that y is not zero and throw an exception if it is.

VB
Public Function Divide(ByVal x As Single, ByVal y As Single) As Single
    If y = 0 Then Throw New ArgumentException("y shouldn't be zero")
    Return x / y
End Function

If a user of the program encounters this error, she might call support -- the developer that used your component -- and say that she received a weird error stating, "Y shouldn't be zero." The developer then searches through his code and can't see where this exception came from. Now let's rewrite our code

VB
Public Function Divide(ByVal x As Single, ByVal y As Single) As Single
    If y = 0 Then Throw New DivisionByZeroException()
    Return x / y
End Function

The developer immediately identifies the cause, since the exception message would be something like "DivisionByZero exception wasn't handled." In the calling code, he would be able to catch and handle this specific kind of exception.

Now the example might seem a little unrealistic, but the general idea is that the existing framework exceptions are too general (they should be) and it is better to provide a more descriptive exception that occurs only in particular situations. One might argue that a descriptive exception message is enough, but in most cases a developer wants to handle different kinds of exceptions differently, such as:

VB
Try 
    Dim z = myComponent.Divide(x, y)
Catch ex As DivisionByZeroException 
    'do something
Catch ex As OverflowException 
    'do something
Catch ex As Exception
    'we don't know what happened
End Try

In addition, you might want to provide more data for the caller. You do that by adding some other properties to your exception class, so that the calling code could inspect these properties and make a decision. We'll discuss this later in the article.

Exception Thrown by Another Component

When we call some other code and this code throws an exception, we are tempted to leave it unhandled and let our caller -- presumably the main application -- handle it. I'll show you why this is not a very good idea. Consider the following piece of code:

VB
Function GetConfigValue(ByVal param As String) As Single
    Return My.Settings.Item(param)
End Function

We can get three different exceptions here. First, if our config file is missing, we'll have a FileNotFoundException. Next, if we don't have a setting identified by the parameter argument, a SettingsPropertyNotFoundException is thrown. Last, if the value can't be converted to a Single, we'll have an InvalidCastException. The first and the third are difficult to understand when you are writing the calling code. If I call a GetConfigValue method, I expect at least an exception related to configuration, not something about files or conversions.

A possible solution would be to create two custom exceptions: MissingConfigFileException and ConfigValueIsNotSingleException. It could be convenient to inherit MissingConfigFileException from FileNotFoundException, since it already has the FileName property that could be used by the developer in identifying the problem. In the same way, ConfigValueIsNotSingleException can be inherited from InvalidCastException. However, this is not really necessary, since we'll provide the original exception as the InnerException property. So, our code becomes:

VB
Function GetConfigValue(ByVal param As String) As Single
    Try
        Return My.Settings.Item(param)
    Catch ex As IO.FileNotFoundException
        Throw New MissingConfigFileException(ex)
    Catch ex As InvalidCastException
        Throw New ConfigValueIsNotSingleException(ex)
    End Try
End Function

And the definitions for the exceptions are:

VB
Public Class ConfigValueIsNotSingleException
    Inherits InvalidCastException

    Public Sub New(ByVal ex As Exception)
        MyBase.New(ex.Message, ex)
    End Sub

End Class

Public Class MissingConfigFileException
    Inherits IO.FileNotFoundException

    Public Sub New(ByVal ex As IO.FileNotFoundException)
        MyBase.New(ex.Message, ex.FileName, ex)
    End Sub

End Class

Typically, you would add more properties to your exception class so that the caller has more information about the exception. For example, we could add the ParameterName and ActualValue properties to our ConfigValueIsNotSingleException class. These properties would be ReadOnly and the corresponding private fields should be set in the constructor.

VB
Public Class ConfigValueIsNotSingleException
    Inherits InvalidCastException

    Public Sub New(ByVal ex As Exception, _
        ByVal paramname As String, ByVal value As Object)
        MyBase.New(ex.Message, ex)
        Me._parameterName = paramname
        Me._value = value
    End Sub

    Private _value As Object
    Public ReadOnly Property ActualValue() As Object
        Get
            Return _value
        End Get
    End Property

    Private _parameterName As String
    Public ReadOnly Property ParameterName() As String
        Get
            Return _parameterName
        End Get
    End Property

End Class

...

Function GetConfigValue(ByVal param As String) As Single
    Try
        Return My.Settings.Item(param)
    Catch ex As IO.FileNotFoundException
        Throw New MissingConfigFileException(ex)
    Catch ex As InvalidCastException
        Throw New ConfigValueIsNotSingleException(ex, _
            param, My.Settings.Item(param))
    End Try

End Function

Let's see how the caller could use the improved exception:

VB
Sub ShowMe()
    Try
        Dim x = GetConfigValue("StringParameter")
    Catch ex As ConfigValueIsNotSingleException
        MsgBox(String.Format("The value of {0} is {1}, which is not Single",_
            ex.ParameterName, ex.ActualValue))
    End Try
End Sub

Clearly, developers can now easily identify what caused the exception. For simplicity, in the following sections we'll omit the two added properties and return to a simpler version of ConfigValueIsNotSingleException.

Reducing the Number of Exception Classes

While having a custom exception class for each exceptional situation can be very helpful for the developer using your component, having a lot of classes can clutter your namespace and bring confusion. Sometimes it is better to define a single exception class for several situations. We can put some additional information identifying the cause of the exception into a property. For example, rather than having ConfigValueIsNotSingleException, ConfigValueIsNotIntegerException, etc., we could have a single class called InvalidTypeInConfigException for all type conversion errors in the configuration-related code. To identify the exact problem, we could have the Reason property, which is enumeration-valued. The class definition becomes:

VB
Public Class InvalidTypeInConfigException
    Inherits InvalidCastException

    Public Sub New(ByVal ex As Exception, ByVal reason As ExceptionReason)
        MyBase.New(ex.Message, ex)
        Me._reason = reason
    End Sub

    Private _reason As ExceptionReason
    Public ReadOnly Property Reason() As ExceptionReason
         Get
             Return _reason
         End Get
    End Property

    Public Enum ExceptionReason
        ConfigValueIsNotSingle
        ConfigValueIsNotInteger
        '...
    End Enum

End Class

The calling code becomes:

VB
Function GetConfigValue(ByVal param As String) As Single
    Try
        Return My.Settings.Item(param)
    Catch ex As IO.FileNotFoundException
        Throw New MissingConfigFileException(ex)
    Catch ex As InvalidCastException
        Throw New InvalidTypeInConfigException(ex,_ 
            ExceptionReason.ConfigValueIsNotSingle)
    End Try
End Function

Exception-related Events

Sometimes you want to notify the caller that an exception is going to be thrown. For example, if the caller has provided invalid data, we could give him a chance to correct the situation. This pattern is used, for example, in the System.Windows.Forms.DataGridView class. If an error happens, the class checks if there is a handler for this event. If the handler exists, it is invoked. If it does not exist, an exception is thrown. The user is responsible for correcting the error in the handler. The event argument provides, among others, the Exception property. The handler can inspect this property and, for example, do some logging. The event argument also provides the ThrowException property, which the handler can set to True or False, depending on whether we actually want to throw this exception or handle it in a more peaceful way.

In order to implement this pattern, we have to construct another class for our event argument. Our class should contain the ReadOnly exception property, the ThrowException property and some other properties that the caller can modify in order to handle the situation. Let's see an example:

VB
Public Class UnhandledConfigExceptionEventArgs
    Inherits EventArgs

    Private _exception As InvalidTypeInConfigException
    Public ReadOnly Property Exception() As InvalidTypeInConfigException
        Get
            Return _exception
        End Get
    End Property

    Private _throw As Boolean = True
    Public Property ThrowException() As Boolean
        Get
            Return _throw
        End Get

        Set(ByVal value As Boolean)
            _throw = value
        End Set
    End Property

    Private _value As Object
    Public Property ActualValue() As Object
        Get
            Return _value
        End Get

        Set(ByVal value As Object)
            _value = value
        End Set
    End Property

    Public Sub New(ByVal exception As InvalidTypeInConfigException, _
        ByVal value As Object)
        Me._exception = exception
        Me._value = value
        Me._throw = True
    End Sub

End Class

Next, the class that contains our GetConfigValue() method should have an event:

VB
Public Event ConfigException As EventHandler(_
    Of UnhandledConfigExceptionEventArgs)

Now, let's see how to invoke this pattern. For simplicity, let's ignore the possibility of having a FileNotFoundException. Our purpose is to let the consumer of our component handle the situation when the configuration value cannot be converted to Single and possibly provide an alternative value.

VB
Function GetConfigValue(ByVal param As String) As Single
    Dim value = My.Settings.Item(param)
    Try
        Return CSng(value)
    Catch ex As InvalidCastException
        Dim ConfigException As New InvalidTypeInConfigException(ex,_ 
            ExceptionReason.ConfigValueIsNotSingle, param, value)
        Dim e As New UnhandledConfigExceptionEventArgs(ConfigException, value)
        RaiseEvent ConfigException(Me, e)
        If e.ThrowException Then
            Throw ConfigException
        Else
            Return e.ActualValue
        End If
    End Try
End Function

If an exception is thrown, we raise the corresponding event. The caller now has an option to handle the event, examine the Exception property and perhaps provide a custom value via the ActualValue property. Let's see it in action:

VB
Sub InvalidTypeInConfigExceptionThrown(ByVal sender As Object, ByVal e As_ 
    UnhandledConfigExceptionEventArgs)
    e.ActualValue = 0
    e.ThrowException = False
    LogExeption(e.Exception)
End Sub

Back to GetConfigValue(). After the event is raised, we examine the ThrowException property. Before the event, it had been set to True, but the handler could have set it to False. If it has, we return the modified value. Hopefully it's been modified to a Single value or we'll have another InvalidCastException. If ThrowException is still True, we throw our custom exception, just as we did in the previous section.

This pattern doesn't make much sense if the developer is the one who calls GetConfigValue(), since the exception can be handled in a straightforward try-catch way. However, if the method is called from within our custom or some third-party component, the developer cannot control the return value of the GetConfigValue() method unless we provide this event. In a perfect world, we should be throwing an exception each time we have something wrong with our environment, so that our GetConfigValue() method can't provide the correct value. However, there might be cases where the user of our component does not want the exception to be thrown, but rather wants the normal flow to be continued using a "fake" method result.

So, you can use this pattern only if our method is called from inside our component. Sometimes it makes sense to prevent the exception from occurring and let the rest of the code be executed.

Exception Messages and Localization

The above exceptions return the same message that the underlying exceptions provide. This could be confusing if the calling code forgot to handle this particular exception. After all, you went this far to provide a custom exception. Why not provide a custom message?

You probably don't want to put the message into your code. A more appropriate place is a resource. Another reason to use it is that your message becomes easily customizable.

So, suppose we added two string resources: ConfigValueIsNotSingleMessage and ConfigValueIsNotIntegerMessage. The message could be a format string, something like "Value {0} of setting {1} is not a single number." We should override the Message property:

VB
Public Overrides ReadOnly Property Message() As String
    Get
         Select Case Reason
             Case ExceptionReason.ConfigValueIsNotSingle : Return _
                 String.Format(My.Resources.ConfigValueIsNotSingleMessage,_
                 ActualValue, ParameterName)
             Case ExceptionReason.ConfigValueIsNotInteger : Return _
                 String.Format(My.Resources.ConfigValueIsNotIntegerMessage, _
                 ActualValue, ParameterName)
         End Select
         Return MyBase.Message
    End Get
End Property

Testing

Of course, we'd love to write unit tests for our custom component. While testing the GetConfigValue method, we should test that the exceptions are thrown when they should be. Testing frameworks usually provide ExpectedExceptionAttribute for the test methods that test throwing exceptions. However, this way we can only verify that our exception is thrown. We, on the other hand, would like to verify that our exception has correct property values. Let's see the test code:

VB
<Test()> _
Sub GetConfigValueThrowsExceptionWithConfigValueIsNotSingleReason()
    Try
        Dim x = GetConfigValue("StringParameter")
        Assert.Fail("InvalidTypeInConfigException has not been thrown")
    Catch ex As InvalidTypeInConfigException
        Assert.AreEqual(_
         InvalidTypeInConfigException.ExceptionReason.ConfigValueIsNotSingle,_
         ex.Reason, "Reason should be ConfigValueIsNotSingle")
    End Try
End Sub

We don't use the ExpectedExceptionAttribute here because we are catching our exception. So, in order to verify that it's been thrown, we use the Assert.Fail statement. If the exception is being thrown, we never reach this line. If the exception is thrown, but it is an exception of another type, we don't catch it and the test fails. If the exception is of the correct type, we catch it and verify all of the relevant properties. In our case, this is the Reason property.

Serializing Exceptions

The base Exception class is marked as Serializable. It means that it could be passed to another AppDomain, or even another application. Although it rarely happens, you should probably be prepared for this scenario. By default, only the inherited members are serialized. Whenever your exception crosses the AppDomain boundary, it loses all the custom fields you added. Preventing this is simple — you override the GetObjectData method for serialization and add a certain protected constructor for deserialization. However, it is very easy to forget these things.

Conclusion

Exceptions don't get much attention from the community, especially from the big guys. One obvious reason is that perhaps we still expect our code to be perfect. An exception is something so irregular and annoying that, while our "right" code should be well-structured and have all other nice qualities, we allow our exceptions to be pretty ugly. Another reason, maybe more subconscious, is a sort of primitive superstition: don't talk about exceptions or they'll come and get ya.

I strongly believe that for every programming pattern out there -- be it GoF, Microsoft or your own -- there should be a corresponding pattern of exception handling and/or raising. At least the exceptions should be made an essential part of the pattern. After all, exceptions are classes, so many existing patterns could be applied to them. Perhaps someday we'll hear about Exception Factories, Exception Strategies and Exception Observers. However, since exceptions are not simple objects, depending on your attitude towards them, they can bring chaos into your elegant design or provide controlled execution flow and information exchange between application layers. Exceptions are there, like it or not, and you should turn them into your allies or they'll quickly become your enemies.

History

  • 9 July, 2007 -- Original version posted
  • 26 July, 2007 -- Article edited and moved to the main CodeProject.com article base
  • 22 September, 2007 -- Added the section on serialization

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Software Developer GeekSoft
Lithuania Lithuania
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralGood Job Pin
merlin98126-Sep-07 5:06
professionalmerlin98126-Sep-07 5:06 
GeneralDesign patterns Pin
technot4-Sep-07 3:13
technot4-Sep-07 3:13 
AnswerRe: Design patterns Pin
Artem Smirnov4-Sep-07 4:04
professionalArtem Smirnov4-Sep-07 4:04 
QuestionPuzzled Pin
Dabas31-Jul-07 12:16
Dabas31-Jul-07 12:16 
AnswerRe: Puzzled Pin
Artem Smirnov1-Aug-07 2:47
professionalArtem Smirnov1-Aug-07 2:47 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.