Code quality, part II: Proper exception handling
One critical part in good-quality code is the way it handles errors. Error handling is very rarely fun to write, but nevertheless, it's one of the things that your code gets judged by. Or, rather: proper error handling rarely gets praise from the end user, but the lack of one causes lots of irritation. The correct approach to error handling is, naturally, very dependent on the nature of the application and the tools used. The following assumes having an exception propagation mechanism (the try/catch/finally constructs available in most modern languages).
The fundamental problem can be formulated like this: The sum of your ability to pinpoint the exact source of the error and your ability to react to it meaningfully is constant. When you're deep down in your call hierarchy, perhaps executing a SQL statement for saving the state of some business object of yours, you can easily tell what happened and where by examining the exception. However, what can you do about it? Retry perhaps, but even that may not be correct at times. You shouldn't pop up dialogs or write to the console either – you don't know the conditions you're running under, and the UI is the caller's responsibility anyway. Well, you let the exception propagate upstream.
Now, you're back in the UI calling code (let's say a button click handler). The exception arrives from your business logic library. Now what? At this point, it's easy to give a meaningful error message ("Saving the foobar failed because of a database error") – but giving useful advice for the next step is harder. Should the user retry? What is the likely cause for the error? Some applications dump the provider error message (such as the SQL Server error, usually available through the exception object), but they're pretty likely to be extremely confusing to the user, and possibly even in the wrong language.
Wrapping your exceptions
It's obvious that just rethrowing the technical exceptions doesn't give you many good choices at the UI level. One reasonable alternative is packaging all the additional information you have into a custom exception class whose InnerException property (something like this exists in every class library) contains the original, technical error message. This way, you can go into extreme lengths of providing context information. The example below illustrates the concept (pseudocode very reminiscent of C#).
public enum DBOperationType { InsertFoobar, UpdateFoobar, DeleteFoobar } public class MyDBException : Exception { public enum DBOperationType FailedOperation; public bool ShouldRetry; public string SqlStatement; } // inside a class ... public void UpdateFoobar(Foobar f) { string sql = CreateFoobarUpdateSql(f); try { ExecuteNonQuery(sql); } catch (DBException d) { MyDBException md = new MyDBException(); md.InnerException = d; md.SqlStatement = sql; md.FailedOperation = DBOperationType.EditFoobar; md.ShouldRetry = IsTimeoutOrDeadlockVictimError(d); throw md; } }
Now, you'd have considerably more information at your disposal when catching a MyDBException in your UI code. You can access the original DBException through the InnerException property.
Not catching what you don't understand
Another important rule: Never catch what you don't understand – or at least rethrow it. The code above only catches DBExceptions. This is usually desired, as catching everything
January 1, 2005
В· Jouni Heikniemi В· 2 Comments
Posted in: Misc. programming
2 Responses
Stephen Gryphon - April 20, 2005
Wrapping in a custom exception is good — it preserves original stack trace (plus creates a new one).
"throw ex;" is very very bad, as it wipes the stack.
However "throw;" is also annoying, as it redirects the last line of the stack trace to the rethrow point.
This can hide the location (if it originated directly inside the try).
Even if the exception originates in another method, where the lower level stack is preserved, you still lose the line inside the current method, which could be a problem if you call it multiple times.
Chris Sims - October 14, 2005
In C# you will need to call the parent Exception constructor since InnerException is readonly. Something like this:
public WordSmithDBException(Exception ex,String sqlStatement, DbOperationType failedOp)
:base (ex.Message, ex) // Make sure parent innner exception is saved
{
this.sqlStatement = sqlStatement;
this.failedOp = failedOp;
}
Leave a Reply