In my last post I discussed how exceptions should be treated as exceptional and not used to control work flow. Now lets take a look at how we can manage exceptions to provide a positive user experience without sacrificing the intent of the code.
To start lets review the general guidelines to exception management:
- Only catch exceptions you expect to occur.
- Only catch exceptions if you are prepared to handle them
- Keep exception management simple. You don’t want exception handling throwing exceptions, or then you loose all information about what really happened in the first place.
- Properly clean up your components in the event of an exception
With these simple rules how could we clean up our existing code?
public class Service
{
public IEnumerable<Entity> GetEntities()
{
try
{
var entities = repository.GetEntities("select * from table …");
return entities;
}
catch (Exception e)
{
Logger.Log(e);
return new Entity[0];
}
}
}
To start we can catch an explicit exception, for example SqlException. We could then wrap the exception with a custom type and provide the specific SQL statement that caused the error. finally we can throw the new exception allowing the exception to bubble up the call stack. it would look something like this
public class Service
{
public IEnumerable<Entity> GetEntities()
{
string sql = "select * from table …";
try
{
var entities = repository.GetEntities(sql);
return entities;
}
catch (SqlException e)
{
var wrapped = new ExtendedException(sql, e);
throw wrapped;
}
}
}
Do you see the changes?
- We are only handling SqlExceptions.
- We handle the exception by wrapping it in a custom exception we defined and adding the SQL statement to the message.
- We then throw the new exception allowing it to bubble up the call stack.
We now have better control of what will happened next. For example our UI code can now distinguish between existing items, no existing items and an exception occurring. A controller action might look like this
public ActionResult Index()
{
var results = new Service().GetModels();
if(results.Any())
{
return View(results);
}
return RedirectToAction("AddNew");
}
Exception handling can be placed in a global action filter that logs the error and redirects the user to an “error occurred” page.
Something we didn’t cover in this example was cleaning up components. This example does require it, but lets take a look at another example which cleans up after itself in the event of an exception.
public IEnumerable<Entity> GetEntities(string sql)
{
IDbCommand command = null;
IDataReader reader = null;
try
{
command = connection.CreateCommand();
command.CommandText = sql;
reader = command.ExecuteReader();
while(reader.Read())
{
yield return reader.ToEntity();
}
}
finally
{
if (reader != null)
{
reader.Dispose();
}
if(command != null)
{
command.Dispose();
}
}
}
Here we are disposing of our command and reader components within the finally block. Notice that we don’t have a catch statement, it’s not needed. Granted we could move the ExtendedSqlException into this block, but I wanted to stick with our original code sample.
One last thing to note. The .Net framework contains the keywork using which allows us to write more expressive code when disposing of components. we can rewrite the above block as
public IEnumerable<Entity> GetEntities(string sql)
{
using (var command = connection.CreateCommand())
{
command.CommandText = sql;
using (var reader = command.ExecuteReader())
{
while (reader.Read())
{
yield return reader.ToEntity();
}
}
}
}
Which is the exact same thing as our previous block, but the code is much more expressive and easier to read.