Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / programming / exceptions

Exception Management: Assumptions and Exceptions

4.77/5 (8 votes)
23 Nov 2017CPOL5 min read 14.4K   138  
Exception handling practices like avoiding null values, writing predictable function signatures, avoiding null reference exceptions, etc.

Introduction

How many times have you come a cross "NullReferenceException" in the production environment and spent long hours to figure out what causes the problem? While doing this, have you met one of the following statements?

Java
return null;

or:

Java
return 0;

or:

Java
return -1;

Exception management is essential and one of the most important topics of the software development but also most underestimated one. Developers can be ignorant about logging and exception management while they develop business requirements. Because, they do nothing in normal conditions. On the other hand, when an exception occurs in production environment, it can take several hours to solve the problem because of lack of logging and badly managed exceptions.

In this article, I'm going to write about couple of exception handling practices like avoiding null values, writing predictable function signatures, avoiding null reference exceptions, etc.

Background

The first question is, why does an exception occur? Software programs have several functions and sub-functions. Each function is executed sequentially and can have input and output arguments. Generally, a function's output becomes another one's input (Figure 1-a). While we are writing a function, we're dealing these input and output variables. We make some assumptions about these variables, consciously or unconsciously. Our code works well if our assumptions work (Figure 1-a). If something happens contradicting our assumptions, then our code behaves unexpectedly (Figure 1-b). To prevent these unexpected behaviours, we usually throw exceptions. So, good exception management starts with the right assumptions.

Figure 1

Image 1

How to Make Assumptions?

Let's suppose that we are given an interface which contains the following two functions. One of these two functions returns null (we don't know which one) and other one throws an exception in case the customer does not exist.

Java
Customer GetCustomerByEmail(String email);
Customer GetCustomerById(int customerId);

And we are using one of these functions:

Java
Customer customer =  customerProvider.GetCustomerByEmail(email);
//Should I check null value?
CallCustomer(customer.PhoneNumber);

Should we check null value before using customer variable? Even if we know that, we might easily forget to check. We might also write unnecessary null-checking blocks which make our code messy. But if we return a nullable type instead of null value, then we can inform developers and force them to check a variable like this:

Java
TNullable<Customer> GetCustomerByEmail(String email);

TNullable<Customer> customer =  customerProvider.GetCustomerByEmail(email);
if (customer.HasValue()){
    CallCustomer(customer.Value.PhoneNumber)
}

Note that Nullable<t> type cannot be used with reference types. So I wrote a TNullable<t> type that can be used with reference types. Luckily, nullable reference types were introduced in C# 8. You don't need to create a new type if you use C# 8. 

And if we know that a function never returns a null value, then we don't have to write unnecessary null-checking blocks.

Now, we can focus on these two functions again and determine which one should return null and which one should throw an exception.

If we are writing GetCustomerById method as a helper method that we’re going to use internally, then we can suppose that this method always takes an identifier of an existing customer data.

Java
//A SaleOrder has always an existing customer.
Customer customer = customerProvider.GetCustomerById(order.CustomerId);

The situation that the customer doesn't exist in the database is an unexpected behavior. So it is good to throw an exception here. Don’t forget to log helpful information with the exception. It is recommended to have custom exception classes as well.

Java
public Customer GetCustomerById(int customerId){
	Customer customer  = db.GetCustomerById(customerId);
	if (customer == null)
		throw new EntityNotFoundException("Customer not found. CustomerId:" + customerId);
	return customer;
}

Now, let's say that we have a web page that can be used to search a customer with an email address.

Java
TNullable<Customer> customer = db.GetCustomerByEmail(txtEmail.Text);

We can't suppose that a customer with a given email address always exists in the database. It's quite possible that any customer does not exist with given email address. In such cases, instead of returning a Customer type, it is better to return a nullable Customer type (TNullable<Customer>) to inform the developer this method might return a null value.

Java
public TNullable<Customer> GetCustomerByEmail(String email){
    Customer customer  = db.GetCustomerByEmail(email);
    return new TNullable<Customer>(customer);
}

When to Handle Exceptions?

Generally but not always true, it is good to handle exceptions in outer parts of call chain. But if an exception occurs in an unimportant or optional operation, you might want to write a warning to a log file and ignore the exception. Then, you might catch the exception inner part of the call chain. Also, you might want to catch exceptions and write informative lines to logger and rethrow exceptions to be handled by outer parts of the call chain.

Using the Code

To demonstrate the idea, that I talked about, I created a sample application. This sample application contains very simple model classes which contain just a few properties that we are going to use.

Image 2

Other files/classes in the project:

  • DBConnection: A database mock class which returns predefined objects
  • TNullable: A class to define Nullable reference types
  • EntityNotFoundException: A custom exception class
  • Example1, Example2, Example3: Three example files. Detailed information is below
  • Program: Entry point of the application. By default Main method something like below:
Java
static void Main(string[] args)
{
	try
	{
		RunExample1(); 
		//RunExample2();
		//RunExample3();                 
	}
	catch (Exception ex)
	{
		LogEx(ex);
	}
	Console.ReadLine();
}

You can run other examples. Just comment out RunExample1() and uncomment one of the other lines.

Example1

Initial code with bad practices. Actually, there is no exception management at all. When you run, you get a "NullReferenceException". You don’t have any clue about what causes that exception.

Image 3

Example2

A better implementation of the example. It throws an exception or returns TNullable object for null values. Now we know that the exception occurred while we load the City reference of the Address. But we still don't know which order's address has an invalid City reference.

Image 4

Example3

In the third example, I added exception handling blocks, wrote helpful information and re-throwed exception. Now we know which order, address and city data causes the exception!

Image 5

Conclusion

I tried to explain how to avoid null references, and write predictable method signatures to help developers to avoid unnecessary null-checking blocks. I hope you liked this article. Please share your opinions and suggestions.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)