The use of exceptions to control program flow (flow control) is a
long -
standing topic . I would like to summarize this topic and give examples of the correct and incorrect use of exceptions.
Exceptions instead of if-s: why not?
In most cases, we read the code more often than we write. Most programming practices are aimed at simplifying the understanding of code: the simpler the code, the less bugs it contains and the simpler its support.
Using exceptions to control the progress of a program masks the programmer’s intentions, so this is considered bad practice.
public void ProcessItem(Item item) { if (_knownItems.Contains(item)) { // Do something throw new SuccessException(); } else { throw new FailureException(); } }
The ProcessItem method complicates code understanding, since it is impossible to say what the results of its execution are possible just by looking at its signature. Such code violates the principle of
least surprise , since throws an exception even in case of a positive outcome.
')
In this particular case, the solution is obvious - it is necessary to replace the throwing of an exception by returning a boolean value. Let's look at more complex examples.
Exceptions for validating incoming data
Perhaps the most common practice in the context of exceptions is to use them in case of receiving invalid input data.
public class EmployeeController : Controller { [HttpPost] public ActionResult CreateEmployee(string name, int departmentId) { try { ValidateName(name); Department department = GetDepartment(departmentId); // Rest of the method } catch (ValidationException ex) { // Return view with error } } private void ValidateName(string name) { if (string.IsNullOrWhiteSpace(name)) throw new ValidationException(“Name cannot be empty”); if (name.Length > 100) throw new ValidationException(“Name length cannot exceed 100 characters”); } private Department GetDepartment(int departmentId) { using (EmployeeContext context = new EmployeeContext()) { Department department = context.Departments .SingleOrDefault(x => x.Id == departmentId); if (department == null) throw new ValidationException(“Department with such Id does not exist”); return department; } } }
Obviously, this approach has some advantages: it allows us to quickly “return” from any method directly to the catch block of the CreateEmployee method.
Now let's look at the following example:
public static Employee FindAndProcessEmployee(IList<Employee> employees, string taskName) { Employee found = null; foreach (Employee employee in employees) { foreach (Task task in employee.Tasks) { if (task.Name == taskName) { found = employee; goto M1; } } } // Some code M1: found.IsProcessed = true; return found; }
What do these two samples have in common? Both of them allow you to interrupt the current thread of execution and quickly jump to a certain point in the code. The only problem with this code is that it significantly degrades readability. Both approaches make it difficult to understand the code, which is why the use of exceptions to control the flow of a program is often equalized with the use of goto.
When using exceptions it is difficult to understand exactly where they are caught. You can wrap a code that throws an exception into a try / catch block in the same method, or you can put a try / catch block several levels higher. You can never know for sure whether this is done intentionally or not:
public Employee CreateEmployee(string name, int departmentId) {
The only way to find out is to analyze the entire stack. Exceptions used for validation make the code much less readable, because they do not clearly show the intentions of the developer.
It is impossible to simply look at such a code and say what might go wrong and how to respond to it .
Is there a better way? Of course:
[HttpPost] public ActionResult CreateEmployee(string name, int departmentId) { if (!IsNameValid(name)) { // Return view with error } if (!IsDepartmentValid(departmentId)) { // Return view with another error } Employee employee = new Employee(name, departmentId); // Rest of the method }
Specifying all checks explicitly makes your intentions much more understandable. This version of the method is simple and obvious.
Exceptions for exceptions
So when to use exceptions? The main goal of exceptions is a surprise! - mark the exception in the application. An exceptional situation is a situation in which you do not know what to do and the best way out for you is to stop performing the current operation (perhaps with preliminary logging of the details of the error).
Examples of exceptional situations can be problems with connecting to the database, the lack of necessary configuration files, etc. Validation errors
are not exceptional , because The method that checks incoming data expects by definition that it may be incorrect.
Another example of the correct use of exceptions is the code contract validation. You, as the author of the class, expect customers of this class to honor his contracts. The situation in which the contract method is not respected is exceptional and deserves an exception being thrown.
How to handle exceptions thrown by other libraries?
Whether the situation is exceptional depends on the context. The developer of a third-party library may not know how to deal with problems connecting to the database, since he does not know in what context his library will be used.
In the case of a similar problem, the library developer is not able to do anything with it, so throwing an exception would be an appropriate solution. You can take the Entity Framework or NHibernate as an example: they expect that the database is always available and if this is not the case, throw an exception.
On the other hand, a developer using the library can
expect the database to go offline from time to time and develop its application with this fact in mind. In case of failure of the database, the client application may try to repeat the same operation or display a message to the user with an offer to repeat the operation later.
Thus, the situation may be exceptional from the point of view of the underlying code and expected from the point of view of the client code. How in this case to work with the exceptions thrown by such library?
Such exceptions should be caught as close as possible to the code that throws them out . If this is not the case, your code will have the same drawbacks as the sample code with goto: it will not be possible to understand where this exception is processed without analyzing the entire call stack.
public void CreateCustomer(string name) { Customer customer = new Customer(name); bool result = SaveCustomer(customer); if (!result) { MessageBox.Show(“Error connecting to the database. Please try again later.”); } } private bool SaveCustomer(Customer customer) { try { using (MyContext context = new MyContext()) { context.Customers.Add(customer); context.SaveChanges(); } return true; } catch (DbUpdateException ex) { return false; } }
As can be seen in the example above, the SaveCustomer method expects problems with the database and intentionally catches all errors related to this. It returns a boolean flag, which is then processed by the code above in the stack.
The SaveCustomer method has a clear signature that tells us that there may be problems in the client saving process, that these problems are expected and that you should check the return value to make sure everything is in order.
It is worth noting the well-known practice applicable in this case: no need to wrap such code in a generic handler. The generic handler claims that
any exceptions are expected, which is essentially not true.
If you really expect any exceptions, you do so for a very limited number of them, for which you know for sure that you can handle them. Placing a generic handler causes you to swallow the exceptions that you did not expect, putting the application in a non-consistent state.
The only situation in which generic handlers are applicable is to place them at the highest level in the application stack to catch all exceptions not caught by the code below in order to log them. Such exceptions should not be attempted to be processed; all that can be done is to close the application (in the case of a stateful application) or stop the current operation (in the case of a stateless application).
Exceptions and fail-fast principle
How often do you come across code like this?
public bool CreateCustomer(int managerId, string addressString, string departmentName) { try { Manager manager = GetManager(managerId); Address address = CreateAddress(addressString); Department department = GetDepartment(departmentName); CreateCustomerCore(manager, address, department); return true; } catch (Exception ex) { _logger.Log(ex); return false; } }
This is an example of incorrect use of a generic exception handler. The code above implies that all exceptions coming from the body of the method are a sign of an error in the process of creating a custom tester. What is the problem with this code?
In addition to the similarity with the “goto” semantics discussed above, the problem is that an exception, a block coming to a catch may not be an exception we know. The exception can be either ArgumentException, which we expect, or ContractViolationException. In the latter case, we hide the bug by pretending that we know how to handle such an exception.
Then, a similar approach is used by developers who want to protect their application from unexpected drops. In fact, this approach only masks the problems, making it harder to catch them.
The best way to work with unexpected exceptions is to stop the current operation completely and prevent the non-consistent state from spreading across the application.
Conclusion
- Throw an exception if and only if you need to declare an exceptional situation in the code.
- Use return values ​​when validating incoming data.
- If you know how to handle exceptions thrown by the library, do it as close as possible to the code that throws them.
- If you are dealing with an unexpected exception, stop the current operation completely. Do not pretend to know how to deal with such exceptions.
Link to original article:
Exceptions for flow control in C #