Sunday, September 16, 2007

Thoughts on Exception Design

Working on my library, I came across an issue that has plagued me in the past. In my quest to provide the most information on failures, I construct my exceptions with detailed messages. This works where book sized messages can be displayed or stored but what happens when the volume of the message hides the relevant details for those that want at it easily? Also, what about exception hierachies, what happens if the same information must be associated with more than one hierarchy?

Here is an example of my dilema - binding values to a database query parameter; three possible error hierarchies can be invoked:

The programmer provided an incorrect index or bind name
SqlException->SqlDatabaseException->SqlProgrammingException

Some database problem prevented the bind
SqlException->SqlDatabaseException->SqlOperationalException

The database interface library caused a problem
SqlException->SqlInterfaceException

I want to supply the following values - Bind index, upper bound for arguments, value to be bound.

Normally, when there is only one hierarchy, I would create a subclass with the required instance variables and a custom toString (or equivalent) implementation. With three hierarchies, I would have to create three subclasses for the same variables, a bit of a waste.

So I decided to do something a bit more generic. I created my base exception class with the concept of properties so you have something like this (pseudo-ish D code):

class Exception {
  typeof(this) setProperty (T)(string key, T value)
  Box          getProperty (key)
  bool         hasProperty (key)
  Box[string]  propertyBag
}

The setProperty returns the type of the exception to allow for chained method calling.

Since index range errors are common developer errors, I decided to create a exception class for that case so I have:

SqlException
  ->SqlDatabaseException
      ->SqlProgrammingException->SqlBindException
      ->SqlOperationalException
  ->SqlInterfaceException


For the standard bind error, I instantiate the SqlBindException as such:

throw (new SqlBindException ("Bind index out of range"))
  .setProperty ("BindPosition", idx)
  .setProperty ("BindType", "TEXT")
  .setProperty ("BindUBound", ubound)
  .setProperty ("BindValue", value)
  .setProperty ("BindValueType", valueType)
  .setProperty ("SQL", sqlText);

If there is a database error, then I can do the following:

throw (new SqlOperationalException ("Something went wrong"))
  .setProperty ("BindPosition", idx)
  .setProperty ("BindType", "TEXT")
  .setProperty ("BindUBound", ubound)
  .setProperty ("BindValue", value)
  .setProperty ("BindValueType", valueType)
  .setProperty ("SQL", sqlText)
  .setProperty ("VendorCode", vendorCode)
  .setProperty ("VendorMsg", vendorMsg);

or if the interface is in an inconsistent state with the database:

throw (new SqlInterfaceException ("The interface is broken"))
  .setProperty ("BindPosition", idx)
  .setProperty ("BindType", "TEXT")
  .setProperty ("BindUBound", ubound)
  .setProperty ("BindValue", value)
  .setProperty ("BindValueType", valueType)
  .setProperty ("SQL", sqlText)
  .setProperty ("VendorCode", vendorCode)
  .setProperty ("VendorMsg", vendorMsg);

With the modified toString, I get the following:

SqlBindException: Bind index out of range

BindPosition: 4
BindType: TEXT
BindUBound: 3
BindValue: testval
BindValueType: string
SQL: select ... from ... where x=?, y=?, z=?;

By separating the properties from the message, I have absolute control over how I will display, or log, the information.

And that helps with the other problem: long messages versus terse description.

The above SqlBindException has a very terse description, the one that is common to sequence type errors. I have seen many instances of 'array bounds out of range' type errors. Of course, with the properties, the message can be terse but the information necessary to track the bug is available.

Let's say, I have the following terse error message:

"Failed to bind parameter"

Not very descriptive. Now let's say that I wanted to provide a default user facing error message for developers to use, or a more descriptive message aimed at developers.

"Failed to bind "~bindType~" parameter, either the statement is finalized, or the database is in an inconsistent state. If the statement is finalized, then the interface is out of sync with the database. If the database is in an inconsistent state, then this may be an interface error or a problem with SQLite. If the problem can be replicated, please log an issue with a test case."

To support long messages, I have added them to the base class:

class Exception {
  typeof(this) setProperty (T)(string key, T value)
  Box          getProperty (key)
  bool         hasProperty (key)
  Box[string]  propertyBag

  typeof(this) setLongMsg (string longMsg)
  string       longMsg;
}

Besides the length of message, long messages differ from the normal exception messages in that they support properties with the "$()" syntax. The above message is set as a long message as follows:

throw (new SqlOperationalException ("Failed to bind parameter"))
  .setLongMsg (
    "Failed to bind $(BindType) parameter, either the statement "
    "is finalized, or the database is in an inconsistent "
    "state. If the statement is finalized, then the interface "
    "is out of sync with the database. If the database is in "
    "an inconsistent state, then this may be an interface "
    "error or a problem with SQLite. If the problem can be "
    "replicated, please log an issue with a test case.")
  .setProperty ("BindPosition", idx)
  .setProperty ("BindType", "TEXT")
  .setProperty ("BindUBound", ubound)
  .setProperty ("BindValue", value)
  .setProperty ("BindValueType", valueType)
  .setProperty ("SQL", sqlText)
  .setProperty ("VendorCode", vendorCode)
  .setProperty ("VendorMsg", vendorMsg);

When the instance property longMsg is invoked, the message is returned as follows:

Failed to bind TEXT parameter, either the statement is finalized, or the database is in an inconsistent state. If the statement is finalized, then the interface is out of sync with the database. If the database is in an inconsistent state, then this may be an interface error or a problem with SQLite. If the problem can be replicated, please log an issue with a test case.

Properties allow me to do something a little different with long error messages. When chaining exceptions, I can construct a long message that uses properties from the chained exception. Eg:

try {
  throw (new SomeException ("Inner exception"))
    .setProperty ("A", aValue);

} catch (SomeException e) {
    throw (new OtherException ("Outer exception", e))
      .setLongMsg (
          "Blah blah blah $(A) blah blah $(B).")
      .setProperty ("B", bValue);
}

I have found that the concept of exception properties to be a valuable tool for exception flexibility; something that is making my life a bit easier.

No comments: