Home > Should we throw exceptions on constructors in C#?
Abdelkrim Bourennane

Should we throw exceptions on constructors in C#?

Should we throw exceptions on constructors in C#?

In this article, we will take a look into some details of throwing exceptions in the constructor of reference types in .NET. We will analyze it from the memory management point of view and also from code maintainability perspective.

You can find all the code written for this article in this GitHub repo.

Let’s dive in.

 

Motivation to throw exceptions in the constructor

 

Throwing an exception is a very common way to halt the execution when something unexpected happens. And when we instantiate new objects it’s possible to use exception to prevent the creation of an object in an invalid state.

For instance, if we define a type that has X and Y properties to represent a position in a grid, the Position object can only be created for values of X and Y that are greater than 0:

 

This example shows a case of preventing developer’s error – prevention from passing negative or nil values when creating the position instance.

In this case we can just avoid passing the forbidden values with prior validation, but in other cases it’s not always that obvious to avoid exceptions being thrown, especially with external packages, code from System.Collections or System.IO.

 

Impact of exceptions on the object lifecycle

 

Now that we have a type to work with, we can check if throwing exceptions in the constructor has any impact on memory management and the way the garbage collector handles it.

To follow the lifecycle of the position object, we can add a finalizer to print to the console when it has been finalized:

 

The finalizer is a special method that is called by the garbage collector before collecting an object. We use them here for a pedagogical reason, but in real-life scenarios they should be left as a last resort as they add some memory overhead. Their behavior depends on the GC (Garbage Collector), they might not be called at all, and they should never be called explicitly.

The proper way to add cleaning logic is to implement (you guessed it!) IDisposable and call it explicitly with the using keyword.

To test our code, we create a console app with a simple main method:

 

The console app does mainly two things, :

  • the first part creates 5 Position objects with invalid parameters,
  • and the second part is a loop to force garbage collection. We use a while loop because we don’t know when the finalizers are called so we will force the collection multiple times until we get what we want.

Alongside the console app, we will use the Memory Profiler to check the state of our heap.

Running the console app until the Console.ReadLine() line will print:

Exception has been thrown
Exception has been thrown
Exception has been thrown
Exception has been thrown
Exception has been thrown

And checking the heap, we see that the position object has been allocated 5 times:

Impact of exceptions on the object lifecycle

 

If we inspect the content of one of the allocated objects:

One of the allocated objects

 

We see that it is initialized with default parameters.

So, although the constructor hasn’t finish running and completed with an exception, the position objects have been allocated.

Continuing with the console app run and by forcing the garbage collection, we get in the console:

Position has been finalized.
Position has been finalized.
Position has been finalized.
Position has been finalized.

and when we check the heap:

initialized with default parameters

 

We can see that 4 instances have been freed and the size in bytes went from 120 to 24. The last object is still referenced so the GC won’t collect it (as we are still in the Main() method).

To summarize this experiment, throwing exceptions in the constructor doesn’t have any impact on the lifecycle of the object who was allocated, initialized and then garbage collected normally.

So, is it always ok to throw exceptions on the constructor? Not so fast.

 

Constructor exceptions and unmanaged resources

 

The example we took was simple and had only managed resources. What happens when unmanaged resources come to play? Let’s figure it out by implementing a class that will allow us to dump the memory to a file MemoryDumper:

 

This class allocates an unmanaged memory of the given size and writes its content to a file. In the constructor we allocate the needed memory, and we open the stream to the destination file. The FileStream will throw an exception if the filePath is empty or null or invalid.

To use this class and dump 1 GB of memory to a file we need to instantiate it and use the using keyword:

 

If you don’t see the issue with this code, let’s break it down to what it really is. This code when lowered is equivalent to this:

 

And in our case, the exception is thrown at the constructor level, so to use this properly we need to wrap the using statement with a try catch:

 

Even if we have prevented our application from crashing, we haven’t fixed the real problem. We can’t call dispose on the object since we can’t reference it. It means that the 1 GB we allocated is not freed (since the exception is thrown after the allocation is done).

If our code is in production and the process is not restarted frequently, that’s an issue that can lead to memory leaks.

To fix it, we have three options:

  • Leave the unmanaged memory allocation as the last operation in the constructor.
  • Make the constructor simple and with no exceptions, and keep using without the try-catch.
  • Implement finalizers, which as we discussed should be avoided.

 

Code Usability concerns

 

Other than the specific case that we might or might not encounter in our life as a .NET developer, exceptions in constructors can have other issues related to code usability and maintainability.

When was the last time you thought of the possibility of having an exception thrown when you used the constructor to instantiate an object from a library? The most probable answer is “never” and that’s for a valid reason.

Not all constructors are implemented equal, most of them will never throw an exception, and for the ones who do, there is no way to know, since the constructor’s signature doesn’t hint to any possible issue and we are left with either reading documentation (if it exists), reading the code of the constructor, or with trial and error.

In addition to that, having multiple constructors that throw exceptions can lead to having multiple try-catch statements all over the code base for every new instantiation. In addition to the extra level of nesting the code might turn difficult to read.

 

Alternatives to throwing exceptions in the constructor

 

Whether we want to still throw exceptions in constructors or not, remains a personal or a team’s choice but it’s good to know the things to consider and the possible alternatives.

 

Throw exceptions early

 

If we want to keep throwing exceptions in constructors, we need to consider the order of the operations. We should throw exceptions as early as possible and leave any allocations or unmanaged resources operations later in the constructor.

This introduces temporal coupling and needs precaution to keep the order of execution that way and usually precautions in code should ring a bell and pushes us to rethink our design choices.

 

Two-phase construction

 

We can split the instance construction into two steps. A good example of this is the SqlConnection class in .NET:

 

We first instantiate the object then we call Open on it to fully have it ready.

The constructor in this way should be lightweight and does the minimum required to get the object to a usable state. Exactly like the constructor of SqlConnection, it has some validation exceptions, but they are thrown first thing in the constructor and no unmanaged resources are used there. Everything happens in the Open() method.

This solution can be good for the example of the unmanaged memory allocation issue we had earlier.

 

Factory Method

 

We can also have a factory method like some .NET types do. Methods like Parse() and TryParse(), unlike the constructor, are factory methods that clearly communicate the possibility of a failure or an exception when called:

 

Key Take-Aways

 

To summarize, we have come to the conclusion that by themselves exceptions in constructors have no effects on memory management and how the GC does its work on the exception of some very specific cases that can cause memory issues when dealing with unmanaged resources.

Apart from those gray zones, throwing exceptions in constructors remains a design choice but if we don’t want to do it there are a couple of alternatives that we explored like two-phase object construction or a factory method that clearly communicate the intention of the possible failure and render the code more readable and self-explanatory.

And if we decide to use exceptions, it’s important to have good exception handling in our code and we should follow the Microsoft’s Best Practices for exceptions.

Offres d'emploi consultant Cloud Paris Lyon Nantes Cellenza

This posts should interest you
Comments
Leave a Reply

Receive the best of Cloud, DevOps and IT news.
Receive the best of Cloud, DevOps and IT news.