How to efficiently break your code by using exceptions?

Forenotes from 2023

This post was originally published on my blog on May 22nd, 2017. It was one of the last posts in my Daj Się Poznać 2017 blog post series 🖋️. This entry was truly a fun one to write! I had seen a lot of coding horrors when in comes to error handling in my 7 years of doing C# and OOP, and wanted to share some examples of what NOT to do... unless you were looking for trouble ;). On a side note, I can now say from experience that languages that encourage you to deal with errors as a normal, explicit flow in your application (usually via a construct such as Result<SuccessType, FailureType>) usually result in a much better experience and way less bugs, headaches, and frustration. F#, Elixir, or Rust are amongst such languages.

How to efficiently break your code by using exceptions?

At first I wanted to call today's post something along the line of "Exceptions should be exceptional" or "keep your exceptions exceptional". However a quick search on Google made me realise I was not the first to think of this catchy title.

Instead, I decided to go with "How to efficiently break your code by using exceptions?" and this made me think of a different way to present the potential negative effects of overusing exceptions in your codebase. Instead of telling you why you should be careful when using exceptions, I will focus on showing you how to use them "efficiently" in order to break your application in no time. I will be using C# for the examples below, but those could be applied to other languages supporting run-time exceptions, like Java for instance.

image_feature_exception_1.jpg

A word of notice

most of the examples below are inspired by production code from real applications that I had the chance to work on over the years. I made up the stories around them though, to add a little dramatic touch to the whole. Do try this at home if you want, but please don't try this at work!

Example 1: Pokemon exception catching (catch 'em all!)

pokemon_exception-150x150.png

The full code of this example is available in this public gist.

Richard is the sole developer of a small, local vindication company. He was tasked to make the integration between:

One of the main functionalities of Vindigator is the IsCustomerAGoodCustomer method that, thanks to "advanced algorithms", determines if a given customer is a good customer or a bad customer:

/*
 * 'Vindigator' is an imaginary 3rd-party library that contains
 * a lot of useful (if you're into the vindication business) APIs.
 */
public static class Vindigator
{
	/// <summary>
	/// Return true if the customer is a good customer, false otherwise.
	/// </summary>
	public static bool IsCustomerAGoodCustomer(Customer customer)
	{
		if (customer == null)
			throw new VindigatorInvalidCustomerException(
				"Something is wrong with your customer!");

		if (customer.AmountOwed < 1000)
			return true;

		return false;
	}
}

Vindigator has its own set of exceptions, for instance VindigatorInvalidCustomerException that gets thrown when something is wrong with a given customer. Richard finally finishes the integration, as shown by the GenerateVindicationReport method below:

public static class MyBusinessApp
{
	public static VindicationReport GenerateVindicationReport(
		Customer customer, ValuationRates rates)
	{
		var report = new VindicationReport { Customer = customer };

		try
		{
			report.IsGoodCustomer =
				Vindigator.IsCustomerAGoodCustomer(report.Customer);

			if (report.IsGoodCustomer)
				report.MoneyOwed = report.Customer.AmountOwed;
			else
				report.MoneyOwed = 
					report.Customer.AmountOwed +
					report.Customer.AmountOwed * rates.PercentageFee;
		}
		catch (Exception)
		{
			// [Richard] if we get an exception here, it means the customer
			// is null! Seems to happen when the main vindication job gets 
			// stuck (damn John!). Return a null report in that case.
			return null;
		}

		return report;
	}
}

During tests, Richard noticed that the scheduled job that calls his GenerateVindicationReport method once a day for each customer is somehow broken. "It must be John's fault, once again!" says Richard. John is the IT administrator of the company and he was the one who set up the scheduled job. John and Richard generally do not get along very well.

Anyway, John's job sometimes passes null customers as the first argument of the method. This obviously causes the IsCustomerAGoodCustomer method to throw an exception. Richard knows his job well and decides to wrap the call in a generic try / catch block, If an exception occurs, meaning the customer passed is null, he will simply return a null report. All is well and the reports are generated perfectly!

A few weeks later though, Richard gets called by his CEO. It appears that the company lost around 70 000 dollars because a lot of reports for indebted customers have been missing! Richard checks the logs of the application, but obviously nothing is there. It is only after 2 stressful days of debugging that Richard realizes that John's job also occasionally sets the second argument of GenerateVindicationReport to null:

var report = GenerateVindicationReport(customer343, null);

This obviously causes the following line to throw a NullReferenceException that gets swallowed by Richard's generic catch block:

report.MoneyOwed =
	report.Customer.AmountOwed +
	report.Customer.AmountOwed * rates.PercentageFee;

This means that during all those weeks, perfectly valid reports for indebted customers were being ignored because of a generic Catch'em all block, leading to a significant loss of money!

This was a very costly and powerful lesson for Richard, who swore never to write such a try / catch block again.

Example 2: short-circuiting exception handling

The full code of this example is available in this public gist.

Months have passed and Richard is now much better at dealing with all types of exceptions, as the following snippet shows:

public static void SetupReportGeneration()
{
	try
	{
		VindicationUtils.CreateVindicationReportDirectory("Customers_Batch1");
		// The rest of the init goes here...
	}
	catch (FolderCouldNotBeCreatedException ex)
	{
		HandleFailureToCreateMainReportDirectory();
		Log(ex);
	}
	catch (Exception ex)
	{
		Log(ex);
	}
}

private static void HandleFailureToCreateMainReportDirectory()
{
	// this method creates a temporary directory to store the reports,
	// in case the main directory couldn't be created
}

private static void Log(Exception ex)
{
	// Log the exception to a file and send an email alert to the admin...
}

All exceptions are logged so that Richard knows exactly what went wrong, and known exceptions are gracefully handled. For example, the custom FolderCouldNotBeCreatedException is being handled by calling the HandleFailureToCreateMainReportDirectory method.

FolderCouldNotBeCreatedException is sometimes thrown by the VindicationUtils.CreateFolder method shown below:

public static class FileUtils
{
	public static string DefaultPath;

	public static void CreateFolder(string path, string foldername)
	{
		// check if the path is valid
		if (!IsPathValid(path))
			throw new FolderCouldNotBeCreatedException(path + "/" + foldername);

		// The rest of the logic to create the folder goes here...
	}
}

The method itself is being used in VindicationUtils.CreateVindicationReportDirectory:

public static class VindicationUtils
{
	// The original method written by Richard
	public static void CreateVindicationReportDirectory(string foldername)
	{
		FileUtils.CreateFolder(
			FileUtils.DefaultPath,
			foldername + "_" + DateTime.Today.ToShortDateString()
		);
			
		// The rest of the logic to create the report directory goes here ...
	}
}

Everything is working well in production, until Alexandro, the new intern of the company, decides to refactor the code a bit. Alexandro's current task is to create a web version of the report generator. One of the methods he wants to reuse to this purpose is VindicationUtils.CreateVindicationReportDirectory. However Alexandro quickly notices that it throws FolderCouldNotBeCreatedException from time to time, and remembers the advice of Mr Smith, his university teacher:

"always handle exceptions as close as possible to their source!"

With this precious knowledge in mind, Alexandro quickly refactors the method to this:

public static class VindicationUtils
{
	// Same method after being refactored by Alexandro
	public static void CreateVindicationReportDirectory(string foldername)
	{
		try
		{
			FileUtils.CreateFolder(
				FileUtils.DefaultPath,
				foldername + "_" + DateTime.Today.ToShortDateString()
			);
		}
		catch(FolderCouldNotBeCreatedException)
		{
			throw new InvalidOperationException(
				"Oops! An error occurred during the creation of the report directory.");
		}

		// The rest of the logic to create the report directory goes here ...
	}
}

Alexandro is proud of his contribution! He followed the Boy Scout Rule and made the codebase a bit cleaner by handling the exception close to the source and by throwing a new exception that explicitly says there was an error during the creation of the directory.

By doing that, Alexandro also unknowingly broke another part of the application by short-circuiting Richard's own exception handling one level above!

However Richard cannot really blame Alexandro for this. Alexandro didn't introduce any breaking change in the interfaces that would cause the compilation to fail. All the methods have the exact same signature as before. And that's the dangerous thing about exceptions:

Throwing exceptions will make your code lie about itself! You cannot trust the code to tell you what exception it will throw or handle.

2023 Addendum: this is why automated tests, including failure paths, are so important to have in your application, as they may sometimes be the only way to detect such a regression early enough.

Moral of today's stories?

I have more examples in mind about how to use exceptions to break your application. Let's keep them for another post though, as this one is getting pretty long already!

Even though the concept of throwing and catching exceptions is pretty simple to grasp, I consider that proper exception handling is one of the most difficult things to achieve in our profession. Don't count on the compiler for helping you with it. Only thorough testing and proper team communication will save you here.

I hope that the two examples above are a good reminder that you should always keep on your toes when using exceptions anywhere (!) in your code base.

Cheers!