12 December 2010

Exception safety in C# – more than just trying and catching

I’m surprised how often exceptions can be a source of bugs. Not just bugs, but nasty, impossible to find bugs that can have maintenance teams chasing their tails for days on end.

Exceptions can be a very effective means of handling errors in a systematic way, but their use can be a pretty delicate operation. If you don’t handle exceptions properly then you can have leaking resources, partially-completed operations and incomplete data which will all serve to destabilise your application.

A robust and consistent approach to managing exceptions is required to ensure that they do not start to cause problems of their own. Ideally, you should seek to ensure that your methods provide a level of exception safety so that objects are still left in a valid state even when an operation fails. This “valid state” may be an error condition that requires some kind of cleaning up, but it should not include a set of open resources and partially updated data.

Exceptions are not part of the contract

When you call a method or access a property it can end up throwing pretty much any type of exception. You may be able to gain some information about the nature of the exception from its type and message properties but this is by no means guaranteed. More importantly, you are unlikely to have any idea of the state that data has been left in or how much processing was undertaken by a method.

In this sense, exceptions are not part of the contract between the interface and consuming application. If an exception is thrown then it means that “all bets are off”. You could have corrupted data, an incomplete process or some open resources that need to be handled. Exceptions simply don’t tell you enough reliable information about what state your application is in.

An attempt was made to address this problem in Java through checked exceptions – i.e. making particular exceptions part of the interface so you have to handle them in your calling methods. Checked exceptions include predictable problems that your application is expected to recover from – e.g. providing a null argument to a method. This approach is not without controversy and checked exceptions are increasingly discredited, mainly because they encourage lazy developers to catch and dismiss checked errors which can conceal problems from the user.

Checked exceptions were left out of C# mainly for reasons around ease of versioning and scalability rather than their potential for mis-use. They make breaking changes more likely in new interface versions, enforce a particular style of implementation and exception handling strategy onto a consuming application and can get very complicated in larger orchestrations of multiple components each with their own checked exceptions.

Providing the exception safety guarantee

Many C# developers think that providing a try-catch statement is enough to handle exceptions, but this is not the case. When you write methods you have a responsibility to ensure that an application can cleanly back out of any error situations that have been caused by your code. This can mean looking after both your resources and data your when things have gone wrong.

Exception safety is a major concern in C++ development where developers do not have the luxury of garbage collection to handle most resource allocation issues. However, C# developers should still be concerned about as garbage collection does not protect them from all resource allocation duties and issues over data integrity are still just as important.

David Abrahams did much to define exception safety for C++ and outlined three different levels of guarantee that can be applied to operations – any code that does not follow any of these guarantees is regarded as unsafe:

  • Strong guarantee, where a failing operation will not affect program state – i.e. the data will be unchanged. This implies an atomic operation that is part of a transaction where a rollback mechanism protects the consuming application when something goes wrong.
  • Basic guarantee, sometimes known as the “weak” guarantee, which only guarantees that resources will not be leaked. There could still be some side-effects with corrupted data.
  • No-throw guarantee, where a method handles all exceptions locally whilst ensuring that the application’s state remains constant.

Although these definitions were created for C++ they are just as relevant to C# development in the .NET framework. Any C# developer who wants to provide a guarantee of exception safety will have to address their handling of resources and data in a consistent way.

Plugging leaking resources

Resource management is the key responsibility in providing exception safe code – no guarantee can be provided without it.

It’s the responsibility of the consumer of a resource to ensure that it gets cleaned up, not the implementer. This is inconvenient as you have to know which resources require disposal – this is not necessarily clear and it is easy to make a mistake and leave resources hanging.

The using statement can be used to ensure that resources are disposed once they have been used, but this does not help if the resource is being returned by a method. In this case you only want the resources to be cleaned up if an exception occurs so the finally clause of a try-catch block is a better choice.

Keeping data integrity

In the strong guarantee scenario you should bear in mind that an exception can happen at any moment, so you should never let a piece of information go until you can store its replacement safely. If you are writing a method that updates an object then you should be careful not to destroy its old representation until the new one is constructed and ready to go.

Normally, database operations should provide strong guarantees as database interfaces always provide some form of commit-rollback structure. The .Net framework provides a general model for managing this transactional behavior across databases, queues and other systems through the System.Transaction namespace.

Filed under C#, Design patterns, Net Framework.