When it’s good to catch(Exception ex)

When handling exceptions, the generally accepted best practice is to only catch exceptions that can be handled properly and also to catch the most specific exception type that is expected.  This is good advice and applies to most situations, but there is also a common scenario when you’ll want to break both of these rules: adding intermediary information. For example, look at the following code snippet where we are deleting a user.

public static void DeleteUser(int userId)
{
    using(var cmd = _connection.CreateCommand())
    {
        cmd.CommandText = "DeleteUser";
        cmd.CommandType = CommandType.StoredProcedure;
        cmd.Parameters.Add("@UserID", SqlDbType.Int).Value = userId;
        cmd.ExecuteNonQuery();
    }
}

In the above example, we may not have exception handling because if an exception occurred, we’re not able to handle it and move on. However, even though we can’t handle the exception, we can help anyone looking into an exception if it’s logged by including additional information.

public static void DeleteUser(int userId)
{
    try
    {
        using(var cmd = _connection.CreateCommand())
        {
            cmd.CommandText = "DeleteUser";
            cmd.CommandType = CommandType.StoredProcedure;
            cmd.Parameters.Add("@UserID", SqlDbType.Int).Value = userId;
            cmd.ExecuteNonQuery();
        }
    }
    catch (Exception ex)
    {
        throw new Exception(String.Format("An error occurred deleting user '{0}'.", userId), ex);
    }
}

In this case, we caught the exception and then threw a new one including the user that we were trying to delete. We could improve the error handling even more by changing the method signature.

public static void DeleteUser(User user)
{
    if (user == null)
    {
        throw new ArgumentNullException("user");
    }

    try
    {
        using(var cmd = _connection.CreateCommand())
        {
            cmd.CommandText = "DeleteUser";
            cmd.CommandType = CommandType.StoredProcedure;
            cmd.Parameters.Add("@UserID", SqlDbType.Int).Value = user.UserId;
            cmd.ExecuteNonQuery();
        }
    }
    catch (Exception ex)
    {
        throw new Exception(String.Format("An error occurred deleting user '{0}, {1}' ({2}).",
                                          user.LastName,
                                          user.FirstName,
                                          user.UserId),
                            ex);
    }
}

Now we’re accepting a User object instead of an integer user id just to help with exception handling. This may sound like overkill to some, but for an application that will be maintained and supported over time, detailed exception handling and logging is critical. Note that in addition to adding the additional fields to the exception handler, we also added a null reference check at the start of the method since we’re accepting a nullable class instance instead of an integer.

There is still an outstanding problem. If we have exception handling code downstream that is catching specifically SqlException then we just broke that code. By catching SqlException and throwing Exception, we’ve made error handling downstream harder. This can be addressed by a helper utility that we use extensively in our projects, ExceptionUtil.Rethrow, as in this example.

public static void DeleteUser(User user)
{
    if (user == null)
    {
        throw new ArgumentNullException("user");
    }

    try
    {
        using(var cmd = _connection.CreateCommand())
        {
            cmd.CommandText = "DeleteUser";
            cmd.CommandType = CommandType.StoredProcedure;
            cmd.Parameters.Add("@UserID", SqlDbType.Int).Value = user.UserId;
            cmd.ExecuteNonQuery();
        }
    }
    catch (Exception ex)
    {
        throw ExceptionUtil.Rethrow(ex,
                                    "An error occurred deleting user '{0}, {1}' ({2}).",
                                    user.LastName,
                                    user.FirstName,
                                    user.UserId);
    }
}

The code here is almost the same, but has a number of distinct advantages.

  • Exception type. Rethrow used here throws a new exception of the same type as the original. That way if we catch SqlException, it will still throw SqlException. This allows exception handlers downstream to continue to catch the specific exception type.
  • Simplified. The Rethrow method also has an overload that accepts a format string and args to fill in the format. This means we can combine creating the exception to throw and string formatting into one call, which is useful as this call combination is so common.
  • Safer. While not visible here yet, the Rethrow method also has internal exception handling surrounding the String.Format call. Therefore, if there is a problem with the format string or one of the arguments being filled into the format string, it will not cause a new exception as String.Format can.

Finally, here is the source for ExceptionUtil.Rethrow, including an overload without string format arguments and one with.

public static class ExceptionUtil
{
	public static Exception Rethrow(Exception ex, string message)
	{
		if (ex == null)
		{
			ex = new Exception("Error rethrowing exception because original exception is <null>.");
		}

		Exception rethrow;

		try
		{
			rethrow = (Exception)Activator.CreateInstance(ex.GetType(), message, ex);
		}
		catch(Exception)
		{
			rethrow = new Exception(message, ex);
		}
		return rethrow;
	}

	public static Exception Rethrow(Exception ex, string message, params object[] args)
	{
		string formatted;

		try
		{
			formatted = String.Format(message, args);
		}
		catch(Exception ex2)
		{
			formatted = message + "\r\n\r\nAn error occurred filling in exception message details:\r\n\r\n" + ex2;
		}
		return Rethrow(ex, formatted);
	}
}

Recent Post by Samuel Neff