Click here to Skip to main content
15,944,733 members
Articles / Programming Languages / C#
Article

Simplifying Exception-Safe Code, Generics Style

Rate me:
Please Sign up or sign in to vote.
3.25/5 (11 votes)
14 Aug 20047 min read 60.7K   273   10   19
Exception correctness can be more easily attainted with the help of generics and C#'s "using" clause.

Introduction

First, a personal fact: I hate writing exception handling code. And I hate it for three reasons: First reason is that I'm lazy. Second reason is that it usually means expanding a simple two-liner body of code into a ten-liner muddle. Third and most important reason is that it's hard to get the damn thing right most of the time. My favorite example comes from a certain Java application that I had to maintain, in which most database access code had finally blocks that had anywhere from two to three levels of nested try-catch blocks within it. And that's just to make sure that the connections and recordsets were properly closed.

So I set out to rid myself of exception handling code once and for all. The first fruit of my labor was an article that I had published here on CodeProject more than a year ago. You might be wondering about the outcome of that experiment, so let me put your mind at ease by assuring you that it totally failed; even I couldn't justify using that library in a production environment. The performance hit was simply unforgivable.

But I haven't given up yet! C# 2.0 beta is here, and with it comes support for Generics. And now, I believe I have found the solution that would rid me of exception handling for all eternity! (Insert evil laugh soundtrack here.)

Before diving into the details, allow me to present an example that would help us put the solution into perspective.

Exceptions In Action: An Example

I'll use the same example I used in my previous article, and that is the example of an instant messaging application. Let's assume that, for our purposes, the list of friends a user has will be stored in a server-side database, with an in-memory representation of this information stored locally. We'll further assume that we have modeled the user as a class named (what else?) User and the database part with a class named UserDatabase. The code for adding a friend might look like the following:

C#
class User
{
    ...

    private string m_name;
    private System.Collections.Generic.List<User> m_friends;
    private UserDatabase m_db;

    ...

    public string Name
    {
        get { return m_name; }
    }

    ...

    public void AddFriend( User friend )
    {
         m_db.AddFriend( this.Name, friend.Name );
         m_friends.Add( friend );
    }
}

This very simple method has two points of failure. The call to m_friends.Add() could throw an out of memory exception, and the call to m_db.AddFriend() could throw various database-related exceptions. If any of those possible failures actually happen, then the state of our application becomes inconsistent, which is a major problem even for this trivial example. Basically, we have two method calls, and they need to succeed as a whole or fail as a whole.

Exceptions To The Rescue?

The next logical thing to do is to fix the code above with exception handling code, which would look like the following:

C#
class User
{
    ...

    public void AddFriend( User friend )
    {
        m_friends.Add( friend );
        try
        {
            m_db.AddFriend( this.Name, friend.Name );
        }
        catch
        {
            m_friends.Remove( friend );
            throw;
        }
    }
}

This works. If m_friends.Add() fails, then m_db.AddFriend() will never be reached, and the exception will be propagated to the caller. If, on the other hand, the call to m_db.AddFriend() fails, then addition to the m_friends collection is rolled back in the catch block, and all is hunky-dory.

One problem though: it's ugly. A two-liner function has grown into a ten liner. This definitely feels like a brute force approach.

And it's not scalable. You can imagine the complexity if, instead of two operations, you needed to execute three. You'll end up with a pile of code that only a mother could love.

The Reality

I have done it, and you must have done it before. We write code and we say we'll fix the exception and error handling later, which usually never gets fixed, and we find ourselves back to:

C#
class User
{
    ...

    public void AddFriend( User friend )
    {
        m_friends.Add( friend );
        m_db.AddFriend( this.Name, friend.Name );
    }
}

What we need to do is find a way to make writing exception-safe code as seamless as possible. And that's what we'll do in the next section.

Introducing The ObjectGuard Library

Bundled with this article is a Visual C# Express solution containing a library called ObjectGuardLib. The library contains two main classes, ScopeGuard and ObjectGuard. I think the best way to introduce this library is to jump right in and rewrite our previous example using it. So, here it goes:

C#
using ObjectGuardLib;

class User
{
    ...

    public void AddFriend( User friend )
    {
        using (ScopeGuard scopeGuard = new ScopeGuard())
        {
            m_friends.Add( friend );
            ObjectGuard guard = 
              ObjectGuardFactory<bool>.Make( m_friends.Remove, friend);
            scopeGuard.Add( guard );

            m_db.AddFriend( this.Name, friend.Name );

            guard.Dimiss();
        }
    }
}

The first thing we do in the code above is create an instance of the class ScopeGuard, which is simply a class that implements IDisposable, and is in itself a collection of IDisposable objects. When used with C#'s using construct, it will call IDisposable.Dispose() on all objects stored in it when the program exits the using block.

Afterwards, we call m_freinds.Add() normally.

In the event of failure, we need to call m_friends.Remove and pass friend to it. So, we create an object of type ObjectGuard using the class factory ObjectGuardFactory.Make(), passing it the method that we need to invoke and the parameters that this method takes. We also need to specify the return type of the method we want to invoke as a template parameter, which is a bool for the method Remove(). So, bool is passed to ObjectGuardFactory.

ObjectGuard also implements IDisposable, so we can add it to scopeGuard, and scopeGuard will invoke ObjectGuard's Dispose method upon exiting the using block.

Now, here's the juicy bit. By default, ObjectGuard's Dispose method would call the method passed to Make. But if ObjectGuard's Dismiss method is called before Dispose, Dispose will do nothing.

Just in case you haven't caught on, let's walk through the sequence of events:

In case m_db.AddFriend() throws an exception:

  1. m_friends.Add() is called.
  2. An instance of ObjectGuard, guard, is created, and stored in scopeGuard.
  3. m_db.AddFriend is called, and it throws an exception.
  4. Program flow exits the using block.
  5. scopeGuard.Dispose() is called.
  6. scopeGuard calls guard.Dispose().
  7. guard.Dispose calls the method m_friends.Remove( friend ), rolling back the effect of the call to m_friends.Add().

In case no exceptions are thrown:

  1. m_friends.Add() is called.
  2. An instance of ObjectGuard, guard, is created, and stored in scopeGuard.
  3. m_db.AddFriend is called.
  4. guard.Dimiss() is called.
  5. Program flow exits the using block normally.
  6. scopeGuard.Dispose() is called.
  7. scopeGuard calls guard.Dispose().
  8. guard.Dispose does nothing.

This leaves the trivial case of m_friends.Add() throwing an exception, in which case nothing happens since the whole block is exited before any side effects take place.

Another Example: Database Connections and Transactions

We have all written something like the code below at one time or another:

C#
public void InsertBuddy( User theUser, User buddy )
{
    SqlConnection conn = null;
    SqlTransaction trx = null;

    try
    {
        conn = new SqlConnection( ... );
        conn.Open();
        trx = conn.BeginTransaction();

        // do some DB manipulation

        trx.Commit();
    }
    catch
    {
        trx.Rollback();
        throw;
    }
    finally
    {
        conn.Close();
    }
}

Rewriting the above code using ObjectGuardLib would produce:

C#
public void InsertBuddy( User theUser, User buddy )
{
    using ( ScopeGuard guard = new ScopeGuard() )
    {
        SqlConnection conn = new SqlConnection( ... );
        conn.Open();

        guard.Add( ObjectGuardFactory.Make( conn.Close );

        SqlTransaction trx = conn.BeginTransaction();
        ObjectGuard trxGuard
            = ObjectGuardFactory.Make( trx.Rollback );

         guard.Add( trxGuard );

         // do some DB manipulation

         trx.Commit();
         trxGuard.Dismiss();
     }
}

I think you would agree that this looks a lot more cleaner, and is definitely more maintainable and scalable. However, there are two points of interest about this example.

First point, notice that, for the ObjectGuard guarding the SqlConnection instance, we created an object and added it directly to the ScopeGuard instance without holding a reference to it. That's because we want the cleanup action (in this case, closing the connection) to take place regardless of how we exit the block.

Second point, notice that, unlike the previous example, we did not need to pass the return type for SqlConnection.Close() and SqlTransaction.Rollback() as a template parameter to ObjectGuardFactory. This was unnecessary since the return type for both methods is void. In fact, unlike C++, passing void to a C# template as a parameter is not supported, and would cause a compilation error.

ObjectGuard can also be used with anonymous methods. For example, what if you also wanted to output an error message if the transaction fails to commit? Simple. Modify the following line:

C#
ObjectGuard trxGuard = ObjectGuardFactory.Make( trx.Rollback );

To read:

C#
ObjectGuard trxGuard = ObjectGuardFactory.Make( delegate
{
    trx.Rollback();
    Console.Error.WriteLine( "Panic Commit failed!" );
});

Limitations Of The ObjectGuard Library

ObjectGuardFactory.Make() is overloaded to take methods with up to four parameters. Modifying the library to support methods with more parameters is left as an exercise for the reader (sorry, but I have always wanted to say that).

Code License

The code is licensed under the Apache License Version 2.0, which basically means you can do anything with it as long as you don't sue me, and as long as you acknowledge that the code was originally made by yours truly. I think that's reasonable, and I hope you would agree.

Acknowledgments

The idea of this article came from an article by Andrei Alexandrescu and Petru Marginean published on the CUJ's experts' forum. The method that the aforementioned authors present, while done in C++, is far more superior and elegant than anything I might attempt to write.

Conclusion

If you have read this far, then allow me to thank you for bearing with me, and I hope you find the included code and article useful. Allow me also to apologize for not including a detailed discussion of the implementation. If you really think that such a discussion would be useful, then drop me a line and I will try to include it in a sequel to this article. Your feedback would be highly appreciated.

And by the way, C# rocks!

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
Web Developer
Saudi Arabia Saudi Arabia
Mohammad Abdulfatah is currently working as a senior software developer in a small software shop where he spends most of his time maintaining tons of C++/Win32 code and writing .Net web applications. He hates having his picture taken for the same reason he freaks out when he hears his own recorded voice: they both don't look/sound like his own self-image/voice. When he is not coding for a living he could be found stuck to his computer at home coding for fun, reading a book, writing on his weblog (http://mohammad.abdulfatah.net/), or pursuing other vanity projects.

Comments and Discussions

 
GeneralAlready seen somewhere Pin
aureldu016-Apr-06 3:13
aureldu016-Apr-06 3:13 
GeneralRedundancy problem Pin
Ornus18-Aug-04 19:52
Ornus18-Aug-04 19:52 
GeneralRe: Redundancy problem Pin
MohammadAbdulfatah18-Aug-04 21:50
MohammadAbdulfatah18-Aug-04 21:50 
GeneralMany potential errors Pin
Sebastien Lorion18-Aug-04 13:28
Sebastien Lorion18-Aug-04 13:28 
GeneralRe: Many potential errors Pin
MohammadAbdulfatah18-Aug-04 22:06
MohammadAbdulfatah18-Aug-04 22:06 
GeneralBad example Pin
Jonathan de Halleux16-Aug-04 22:02
Jonathan de Halleux16-Aug-04 22:02 
GeneralRe: Bad example Pin
MohammadAbdulfatah16-Aug-04 23:04
MohammadAbdulfatah16-Aug-04 23:04 
GeneralRe: Bad example Pin
Jonathan de Halleux16-Aug-04 23:12
Jonathan de Halleux16-Aug-04 23:12 
GeneralRe: Bad example Pin
MohammadAbdulfatah16-Aug-04 23:20
MohammadAbdulfatah16-Aug-04 23:20 
GeneralRe: Bad example Pin
Jonathan de Halleux16-Aug-04 23:40
Jonathan de Halleux16-Aug-04 23:40 
GeneralBad habit Pin
Jonathan de Halleux16-Aug-04 2:40
Jonathan de Halleux16-Aug-04 2:40 
GeneralRe: Bad habit Pin
MohammadAbdulfatah16-Aug-04 11:10
MohammadAbdulfatah16-Aug-04 11:10 
GeneralRe: Bad habit Pin
Jonathan de Halleux16-Aug-04 22:04
Jonathan de Halleux16-Aug-04 22:04 
GeneralRe: Bad habit Pin
MohammadAbdulfatah16-Aug-04 22:46
MohammadAbdulfatah16-Aug-04 22:46 
GeneralRe: Bad habit Pin
Anonymous26-Jan-05 14:13
Anonymous26-Jan-05 14:13 
GeneralI don't see how Pin
Jörgen Sigvardsson15-Aug-04 7:37
Jörgen Sigvardsson15-Aug-04 7:37 
GeneralRe: I don't see how Pin
MohammadAbdulfatah15-Aug-04 8:46
MohammadAbdulfatah15-Aug-04 8:46 
GeneralRe: I don't see how Pin
Jörgen Sigvardsson15-Aug-04 8:52
Jörgen Sigvardsson15-Aug-04 8:52 
GeneralRe: I don't see how Pin
MohammadAbdulfatah15-Aug-04 9:06
MohammadAbdulfatah15-Aug-04 9:06 

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.