The Leaky Exception Problem
Previously on Code Coach Daily Tips, we’d discovered that Interfaces Are Abstractions. In the article on Avoiding Leaky Abstractions, we studied how interfaces could easily leak implementation details that they are meant to abstract away.
Abstractions can also leak in another, non-obvious way: Exceptions. Why non-obvious? Exceptions do not follow the standard flow of control in programming – they invisibly jump up the call stack until they are caught. Interface definitions are not proving helpful here – they are silent on which exceptions may be throw by implementers.
Let’s go back to our ICustomerRepository example. Here is the latest version that didn’t leak implementation details:
public interface ICustomerRepository { BusinessLogic.Customer GetCustomer(); void AddCustomer(BusinessLogic.Customer customer); void UpdateCustomer(Guid customerId, BusinessLogic.Customer customer); }
In our previous example, we had two adapters, SqlServerCustomerRepository and MongoDbCustomerRepository that implemented ICustomerRepository.
Now, calls to databases sometimes experience transient faults. These are temporary problems that resolve themselves within a few seconds or so. Such failures cover connectivity issues, timeouts, deadlocks, etc.. To handle transient errors, we should silently retry the database operation instead of surfacing them as application errors.
Note: This is not a problem with unrecoverable database errors. We can do little about them. Usually, they bubble up to the global exception handler for diagnostic logging.
OK, back to database transient faults. SQL Server raises transient errors as SqlException. Here is how we might catch and handle them:
try { CustomerRepository.AddCustomer(customer); } catch (SqlException sqlEx) { // code to identify whether it's a transient, recoverable error, // and if it is we can retry the database operation. Otherwise // the exception is not recoverable and we rethrow it. }
Can you see the problem?
The SQL-ness of our SqlServerCustomerRepository adapter has leaked through our ICustomerRepository interface abstraction! How? Easy – the SqlException is not part of the ICustomerRepository interface definition.
The problem becomes even more pronounced when we switch to a Mongo DB ICustomerRepository adapter:
try { CustomerRepository.AddCustomer(customer); } catch (SqlException sqlEx) { // code to identify whether it's a transient, recoverable error, // and if it is we can retry the database operation. Otherwise // the exception is not recoverable and we rethrow it. } catch (MongoDbException mongoEx) { // code to identify whether it's a transient, recoverable error, // and if it is we can retry the database operation. Otherwise // the exception is not recoverable and we rethrow it. }
Hold on – now we have a second exception handler?! This exception leaking is getting out of hand. Does this mean that we need to catch a separate exception for every ICustomerRepository implementer? Yes, pretty much.
This approach is not in alignment with the SOLID Open-Closed Principle (OCP), which states that we should write new code rather than modify existing code, if possible.
OK, how do we fix this?
Tune in tomorrow to find out!
Leave a Reply
Want to join the discussion?Feel free to contribute!