The Case against Throwing ArgumentException and the Like

by Zoran Horvat

Introduction

We are accustomed to guidelines such as these:

  • Throw IndexOutOfRangeException when caller attempted to access an element outside bounds of the data structure, or
  • Throw ObjectDisposedException when a call is made on a member of the disposed object, or
  • Throw ArgumentNullException when the caller has passed a null parameter to the method.

But throwing an exception is only half of the story. The other half is handling it.

In this article we will only analyze the effect of throwing application-specific exceptions, especially those that are telling out the problems in arguments and object state.

Handling Exceptions the Traditional Way

Let’s take a look at an example:

try
{
    container[3] = source.Calculate(data);
}
catch (ArgumentNullException)
{
    // Now what?
}
catch (ArgumentException)
{
    // Now what?
}
catch (IndexOutOfRangeException)
{
    // Now what?
}
catch (ObjectDisposedException)
{
    // Now what?
}

This short piece of code is clearly depicting all the futility of throwing argument- and state-related exceptions. They are meaningless on the receiving end. Let’s examine that in more details.

In the example above, we are making calls on two objects:

container[3] = source.Calculate(data);

First, we are calling the Calculate() method on the source object. This method receives an argument, and we are passing some object named data. Now, this Calculate() method is returning a result, and we are passing that result to the indexer property of the container object, telling that the value should be set on index value 3 inside the container object.

But it seems like both of the objects may throw exceptions from these calls. The Calculate() method may throw ArgumentNullException if data is null. It may also throw ArgumentException if data object does not conform to some other conditions that must also be satisfied. Further on, both of the objects, source and container, may throw ObjectDisposedException if they have been disposed before the call was made. Finally, container object’s indexer property setter may throw IndexOutOfRangeException if it cannot store data at index position 3.

Therefore, we have added a catch block for each of these concrete exceptions. Note that, in the best tradition of exception handling, we are not attempting to handle any other kind of exceptions, such as IOException or FormatException and the like. If we can’t provide a meaningful handling logic for an exception, then we don’t catch it in the first place.

Anyway, here we are, stuck with four exception handlers, thinking what possibly could we do in each of the catch blocks.

Sanitizing the Null Argument after an Exception is Thrown

Let’s focus on just one of the catch blocks in the example and try to see what it takes to implement it. We will stick with the ArgumentNullException because it relates to an obvious case of passing null parameter to the method. Here is the isolated piece of code which deals with null parameters:

try
{
    container[3] = source.Calculate(data);
}
catch (ArgumentNullException) // data have been null
{
    // Fix the argument
    data = "default";

    // Repeat the operation
    container[3] = source.Calculate(data);

    // ... and hope nothing is thrown this time
}

To understand this piece of code, we have to start from the rule of thumb: Do not catch exceptions you cannot handle; if you catch an exception, correct the issue.

This piece of code then demonstrates what it would mean to handle the ArgumentNullException. When this exception is caught, we conclude that data is null and then assign it a non-null value. After that, we repeat the call that previously failed, hoping for the best.

However, we will soon find out that this implementation is not the best we could come up with. First of all, it hopes that there will be no other issues. But other issues are equally probable as the first one, with null argument. For example, Calculate() method might also throw ArgumentException, in case that the corrected argument does not conform with some other rules. What then? Repeated call would have to catch that exception then:

try
{
    container[3] = source.Calculate(data);
}
catch (ArgumentNullException) // data have been null
{
    // Fix the argument
    data = "default";

    // Repeat the operation
    try
    {
        container[3] = source.Calculate(data);
    }
    catch (ArgumentException)
    {
        // Now what?
    }
}

This time, we have nested try-catch blocks. The problem with this construct is that nesting may easily go the other way around – not likely, but what if the ArgumentException gets thrown before ArgumentNullException? Then we have to repeat the catch block on the outer level as well:

try
{
    container[3] = source.Calculate(data);
}
catch (ArgumentNullException) // data have been null
{
    // Fix the argument
    data = "default";

    // Repeat the operation
    try
    {
        container[3] = source.Calculate(data);
    }
    catch (ArgumentException)
    {
        // Handle argument format
    }
}
catch (ArgumentException)
{
    // Handle argument format
}

The problem with this implementation is that it contains code duplication. Part indicated as “Handle argument format” comment is the duplicated piece. Therefore, handle-and-repeat pattern does not seem to be a good idea because it leads to complicated and duplicated code.

There is also one additional problem here. When handling ArgumentNullException, we are assuming that the argument in question is the data variable. Needless to say, this assumption might not hold. If Calculate() method invokes any other methods internally, and accidentally passes null in, then that other method might throw ArgumentNullException instead. It may easily be that data variable was not null in the first place:

try
{
    container[3] = source.Calculate(data);
}
catch (ArgumentNullException ex) when (ex.ParamName == "value")
{
    // Fix the argument
    data = "default";

    // Repeat the operation
    container[3] = source.Calculate(data);

    // ... and hope nothing is thrown this time
}

This attempt is using exception filters, a feature added to C# 6. We are making sure that only if the argument named value has been null. Unfortunately, this is not much better than the first attempt – argument to the nested method call could also be named value, something we simply do not know.

Net result is that we can only conclude that it is far from easy to handle ArgumentNullException. In the remainder of this article, we will show that it is even worse than that. In fact, we will show that it doesn’t make sense to handle ArgumentNullException in the first place.

Sanitizing the Null Argument in Advance

After the previous analysis, it looks like the only reasonable way to deal with null argument is to ensure the variable is not null before making the call. Reacting to such condition as null argument makes no sense, because we already have the variable at our hands – why passing null to a method if we know for sure that it will fail?

if (data == null)
    data = "default";
container[3] = source.Calculate(data);

There is nothing wrong with this code. Note that setting the data variable to literal value “default” is the same thing we did in the ArgumentNullException handler above. However, this time we are testing the condition proactively, rather than waiting for an error to occur and then trying to figure that it was the data variable that caused it.

This approach is deterministic in sense that we do not have to depend on things that are beyond our control, such as argument names in methods that are getting called. It is also cleaner in sense that we don’t have try-catch construct anymore. It is also avoiding code duplication, since other exceptions, such as ArgumentException or IndexOutOfRangeException are now a separate issue.

Finally – and I might tell this is the most important aspect of this solution – we are in full control of the argument validation. This means that we are supposed to perform the check if and only if we are not certain about the result of the check. This may puzzle you, but it’s really simple. If we know that the data variable is not null, then we simply do not check it and that is all. Take a look at this implementation:

class Worker
{
    public DataSource Source { get; } = new DataSource();
    public DataContainer Container { get; } = new DataContainer();

    // Should never return null
    private string PrepareData() => "something";

    public void Work()
    {
        string data = this.PrepareData(); // Non-null for sure
        this.Container[3] = this.Source.Calculate(data);
    }
}

This class repeats the same line as we had before, with passing the data variable to the Calculate() method and then assigning the result to the container at index 3. However, it does not test whether the data are non-null. How comes?

The trick in this implementation is that the Worker class is in full control of the process of initializing the data variable. Although data are coming as the return value of the PrepareData() method, which in theory could return null result, the Worker class is the implementer of the PrepareData() method and it guarantees this method will never return null. Therefore, we don’t have to check against null in the Work() method.

That is how we can write shorter and even more efficient code by entailing certain facts related to the state of objects we are dealing with.

But what if our assumptions are wrong? Let’s see that as well.

Failed Assumption Due to a Bug

Consider the following implementation:

class DataSource
{
    public int Calculate(string value)
    {
        if (value == null)
            throw new ArgumentNullException(nameof(value));
        return 42;
    }
}

class Worker
{
    public DataSource Source { get; } = new DataSource();
    public DataContainer Container { get; } = new DataContainer();

    // Should never return null
    protected virtual string PrepareData() => "something";

    public void Work()
    {
        string data = this.PrepareData(); // Non-null for sure
        this.Container[3] = this.Source.Calculate(data);
    }
}

class BetterWorker : Worker
{
    // The bug: This implementation returns null
    protected override string PrepareData() => null;
}

Almost everything is the same as before. DataSource class is throwing ArgumentNullException when incoming value is null. On the other hand, its consumer, the Worker class, is declaring its PrepareData() method as protected virtual. And although the method comes with an informal guarantee that it will never return null, the derived class, BetterWorker, is coming with implementation which violates that guarantee.

Now we have a problem. Worker class is not checking the data variable for null in its Work() method, because it assumes it can never be null, but then DataSource object’s Calculate() method throws ArgumentNullException.

The question here is: Should we do anything in the Worker class? And, if yes, what?

In that respect, we once again recognize two possibilities:

  • One is to catch ArgumentNullException, because that is what we could expect to come out from the Calculate() method call, or
  • Check whether the data variable is null before calling the Calculate() method

And there we go again, finding ourselves at the beginning of this article. However, there is a difference this time, and that is what we will examine next.

Accepting It: ArgumentNullException Comes From a Bug

Code given above, with derived BetterWorker class which effectively prepares an unacceptable null argument for the Calculate() method, is interesting in sense that BetterWorker class this time contains a bug. There is no better explanation than that.

And that is what makes it different from a general case of passing a null argument to the method. Therefore, we can say that having a null reference passed to the method actually comes from a bug, rather than an intention.

Then, what can we do? For one thing, we have to fix the bug. But that only comes after we detect its presence, of course. Although it is possible to discover defects by applying analytical techniques, it is still normal some of them to stay undiscovered and to ultimately show up only at run time.

In the next section we will examine one formal approach to proving that code is correct, approach based on the Design by Contract theory.

Introducing Method Preconditions and Postconditions

Let me introduce a different idea then. It comes from the Design by Contract theory, which you can learn from Bertrand Meyer’s seminal book Object-Oriented Software Construction – also known by its acronym OOSC. Design by Contract (DbC) says that methods should define their preconditions and postconditions. Those are the claims that must hold true before method execution begins (preconditions) and just prior to returning from the method (postconditions). There are also class invariants – claims that must hold true at all times, except possibly during execution of a public method; however, as soon as a call to a public method completes, all class invariants must be restored.

Let’s take a look at the example at our hands. I will add comments to code to indicate preconditions and postconditions in their respective positions:

class DataSource
{
    public int Calculate(string value)
    {
        // PRECONDITION: value != null
        if (value == null)
            throw new ArgumentNullException(nameof(value));
        return 42;
    }
}

class Worker
{
    public DataSource Source { get; } = new DataSource();
    public DataContainer Container { get; } = new DataContainer();

    protected virtual string PrepareData()
    {
        // POSTCONDITION: return value != null
        return "something";
    }

    public void Work()
    {
        string data = this.PrepareData();
        // PrepareData() returns non-null => data != null

        // Test precondition: data != null
        this.Container[3] = this.Source.Calculate(data);
    }
}

class BetterWorker : Worker
{
    protected override string PrepareData()
    {
        // INHERITED POSTCONDITION: return value != null
        return null;    // This line violates the postcondition
    }
}

Now we can analyze the situation from the point of view of contracts between classes. The first class in line is DataSource. Its Calculate() method defines a precondition, telling that the input value must be non-null. Should anyone violate the precondition by passing null to this method, the result would be ArgumentNullException thrown back.

Now this precondition transfers to all the callers of the DataSource class. They all must act in direction of satisfying this precondition, or otherwise they will face the exception. We have already seen how futile it is to try to recover from precondition failures through exception handling. Therefore, we will be better off ensuring that the callers are satisfying the precondition prior to making a call.

And so we come to the Worker class, which now attempts to ensure that the data variable in the Work() method is non-null. It does so by defining a postcondition to the PrepareData() method. Postcondition is telling the same thing as the precondition to the Calculate() method – it says that the value it returns will be non-null.

However, there is the difference between precondition and postcondition implementations here. Apparently, precondition is implemented as throw exception instruction, and postcondition is, well, not implemented at all. That is something we will have to deal with. It would make sense to test return value against null just prior to returning it and, if it indeed turns to be null, to throw exception again. Here is the principal (not too smart, though) solution which does precise that:

class Worker
{
    // ...
    protected virtual string PrepareData()
    {
        string returnValue = "something";

        // POSTCONDITION: return value != null
        if (returnValue == null)
            throw new Exception("Postcondition failed.");
        return returnValue;
    }
    // ...
}

Anyway, we have to mention the derived BetterWorker class in this context. This class is in fact the source of troubles here. It overrides the PrepareData() method and replaces its implementation with another one. In terms of Design by Contract, preconditions and postconditions are inherited. Even more, derived class cannot add new preconditions, or otherwise it would violate the Liskov Substitution Principle (LSP) . But that is another story.

What we have in this situation here is that overridden implementation of the PrepareData() method failed to repeat postcondition check. Let’s fix that:

class BetterWorker : Worker
{
    protected override string PrepareData()
    {
        string returnValue = null;  // Here we have the bug

        // INHERITED POSTCONDITION: return value != null
        if (returnValue == null)
            throw new Exception("Postcondition failed.");
        return returnValue;
    }
}

With this change, we have finally ensured that any worker object, whether it is of base Worker class or the derived BetterWorker class, will never pass null argument when invoking the Calculate() method. It could happen that Work() method would throw instead, but that is completely different this time. The point is that now we have a clear indication that the worker object contains a bug. We are not letting that bug propagate further in terms of calling the Calculate() method on the DataSource class. That is how preconditions and postconditions are helping us establish better control of our code.

We could do more here. Postcondition checks in the Worker and BetterWorker classes are duplicated. We can implement postcondition inheritance by pulling it out into a non-virtual method, for example, leaving it all to the base class. In addition, we could introduce a special-purpose exception to be used in case when we discover a run-time bug. Here is the entire implementation of base and derived classes:

class DefectDetectedException : Exception
{
    public DefectDetectedException(string message)
        : base(message)
    {
    }
}

class Worker
{
    public DataSource Source { get; } = new DataSource();
    public DataContainer Container { get; } = new DataContainer();

    protected virtual string PrepareData() => "something";

    private string SafePrepareData()
    {
        string returnValue = this.PrepareData();

        // POSTCONDITION: return value != null
        if (returnValue == null)
            throw new DefectDetectedException("Postcondition failed.");
        return returnValue;
    }

    public void Work()
    {
        string data = this.SafePrepareData();
        // PrepareData() returns non-null => data != null

        // Test precondition: data != null
        this.Container[3] = this.Source.Calculate(data);
    }
}

class BetterWorker : Worker
{
    protected override string PrepareData() => null; // The bug
}

This time, the PrepareData() virtual method contains no postcondition checks. Those are located in the SafePrepareData() method instead, and this method in turn is not virtual. Therefore, derived class cannot forget to repeat postcondition check this time. When the time comes to effectively call any of these methods, and that happens in the Work() method, the Worker base class implementation calls SafePrepareData() and in that way makes sure that postcondition check will be triggered before the value is produced for subsequent operations.

And inside the SafePrepareData() we can see that the new DefectDetectedException is thrown if the method would attempt to return null reference. In that way we are clearly indicating to the ultimate caller that there is nothing wrong with data or with communication or with whatever we are doing. Instead, we are telling that there is a bug in code and that should be communicated to whoever is in charge of maintaining the code base.

Should We Throw Detailed Exceptions Then?

We can now turn our attention back to the root of all problems – the DataSource class, which used to throw exceptions when data are not right. Let me inflate this class a bit, so that now it can throw a couple of other exceptions as well:

class DataSource : IDisposable
{

    private bool isDisposed;

    public int Calculate(string value)
    {
        // PRECONDITIONS:
        // object not disposed
        // value != null
        // length of data at least 3
        if (this.isDisposed)
            throw new ObjectDisposedException(nameof(DataSource));
        if (value == null)
            throw new ArgumentNullException(nameof(value));
        if (value.Length < 3)
            throw new ArgumentException("Insufficient data.", nameof(value));

        return 42 + value.Length3;
    }

    public void Dispose()
    {
        this.Dispose(true);
    }

    protected virtual void Dispose(bool disposing)
    {
        this.isDisposed = true;
    }
}

This time, the Calculate() method comes with three preconditions. The holding object must not be disposed and the argument must be a non-null string at least three characters long. This implementation is in line with guidelines we normally find in the open literature. Each precondition is connected with a specific exception type, such as ArgumentException in case that length of data is not sufficient.

But why throwing such specific exceptions? Is the caller really going to handle these concrete exception types? We have already concluded that the caller cannot truly recover from an error by handling, say, ArgumentException. If the caller knew that the argument is incorrect, it should have either corrected it up-front or skipped the call to the Calculate() method entirely. There is no value in ArgumentException, that is the sad true.

And therefore, we could ask why should any class throw any particular exception from the precondition/postcondition check? That could be misleading because the caller may then conclude that it is entitled to handle the exception, ArgumentException for instance. The truth is that the caller was supposed to ensure that precondition holds before making the call, rather than react on a failure after the fact.

So, we could even come up with a solution which throws contract-related exceptions of unknown type. The following code is purely experimental, and I wouldn’t add it to regular code. Anyway, it depicts the idea of throwing an exception of a type which is unknown to the caller:

class DataSource : IDisposable
{

    private class PreconditionException : Exception
    {
        public PreconditionException(string message)
            : base(message) { }
    }

    private bool isDisposed;

    public int Calculate(string value)
    {
        if (this.isDisposed)
            throw new PreconditionException("isDisposed == false");
        if (value == null)
            throw new PreconditionException($"{nameof(value)} != null");
        if (value.Length < 3)
            throw new PreconditionException($"{nameof(value)}.Length >= 3");

        return 42 + value.Length3;
    }

    public void Dispose()
    {
        this.Dispose(true);
    }

    protected virtual void Dispose(bool disposing)
    {
        this.isDisposed = true;
    }
}

Do you see the difference now? DataSource class now contains definition of the PreconditionException – and that is the nested private class. This means that the caller cannot catch this particular exception. The only way for the caller to catch this exception would be to catch the general Exception, which it should never do anyway. Namely, there is no way to properly handle an Exception object, because it can represent any failure condition, including out of memory or stack overflow. Therefore, application should never handle base Exception unless that is the last step in trying to terminate gracefully.

Bottom line is that this time the calling code cannot write catch block like this:

try
{
    this.Container[3] = this.Source.Calculate(data);
}
catch (DataSource.PreconditionException) // Error
{
    // Recover
}

This code does not compile, simply because PreconditionException is a private nested class and no one outside the DataSource class can refer to it. And that is a good thing, because the caller should not be possible to attempt to handle this exception anyway. The caller’s duty is to ensure that the object and its method arguments are all in good condition before making the call. If it missed something in that phase, no exception handler will ever make the situation any better.

However, there is one issue here. As it seems right now, some of the preconditions implemented in the Calculate() method are opaque. Asking the caller to pass non-null string is fine, I guess. But testing whether the string is at least three characters long might not be that obvious to the caller. In that respect, the caller might object that the game is not fair anymore. That is precisely the issue we will tackle next.

Making Preconditions Discoverable by the Caller

With proper preconditions implemented in the feature provider, the ball is now in the caller’s yard. Feature consumer must ensure that all preconditions hold prior to making a call to a method. And so the consumer has every right to know what those preconditions are.

For example, the first precondition in the Calculate() method of the DataSource class is that the object must not be disposed. The problem here is that the caller might not know whether the object is disposed or not. It may have received the entire object as its own method argument. How can it ensure that the object is not disposed then, when the object may have already been disposed before it has got hold of it?

The solution is to turn preconditions only consist of calls made to members that are accessible to the consumer. In case of the Dispose pattern, this means that the provider, DataSource class, should not rely on its private isDisposed field because that field is inaccessible to the caller. We can fix the issue right away by turning this isDisposed flag publically readable:

class DataSource : IDisposable
{
    // ...
    public bool IsDisposed { get; private set; }

    public int Calculate(string value)
    {
        if (this.IsDisposed)
            throw new PreconditionException("IsDisposed == false");
        // ...
    }

    public void Dispose()
    {
        this.Dispose(true);
    }

    protected virtual void Dispose(bool disposing)
    {
        this.IsDisposed = true;
    }
}

With this change, part of the contract regarding disposing has become the public part of the DataSource class. The information is now publically shared with the consumers, and consumers can perform tests on their own if they are not certain about the state of the system. For example, we might introduce another method in the client:

class Worker
{
    // ...
    public bool TryWorkWith(DataSource source)
    {
        if (source.IsDisposed)
            return false;

        string data = this.SafePrepareData();
        this.Container[3] = source.Calculate(data);
        return true;
    }
}

In this case, Worker object receives DataSource object from the outside. It cannot tell whether the object has been disposed, but it can ask the object itself and find it out.

We could do the same with other preconditions of the Calculate() method. It could turn argument test into a public method, so that the consumer can test that as well:

class DataSource : IDisposable
{
    // ...
    public bool IsApplicableTo(string value) =>
        value != null && value.Length >= 3;

    public int Calculate(string value)
    {
        // ...
        if (!this.IsApplicableTo(value))
            throw new PreconditionException($"IsApplicableTo({nameof(value)})");
        // ...
    }
    // ...
}

With this change, the consumer can even safely work with a string it hasn’t constructed alone:

class Worker
{
    // ...
    public bool TryWorkWith(string data)
    {
        if (!this.Source.IsApplicableTo(data))
            return false;

        this.Container[3] = this.Source.Calculate(data);
        return true;
    }
}

This method will never fail, simply because it is testing whether the data are satisfying the preconditions before making the call. If data would cause precondition violation, then the method just returns False, thus quitting further execution before it comes to calling the Calculate() and causing precondition violation.

Note, however, that this method is not testing the IsDisposed flag on the Source object. The reason for not doing so is that the Source object is encapsulated inside the Worker class. Therefore, this object already guarantees that the Source object has not been disposed, leading to the conclusion that there is no need to test the IsDisposed flag before making a call to the Source object. That is how contracts are helping us establish highest stability with least amount of code.

Conclusion

In this article we have analyzed behavior of the feature provider and its consumer in respect to error conditions. We have demonstrated that throwing state-related exceptions, such as ArgumentException, ArgumentNullException, ObjectDisposedException, IndexOutOfRangeException and the like is not a good course of action.

It turns that these specific exceptions are of little use at the consuming end. And, although we can easily catch each of the exceptions, we cannot truly provide any meaningful handling logic. The caller cannot recover from failure for the same reasons that prevented it from making the correct call in the first place.

If the caller knew that the argument must not be null, and if it knew that the variable it holds is null, then it wouldn’t pass that variable to that method, right? Well, the fact that the caller did make an incorrect call means that it was completely unaware that things have gone beyond its control. That is precisely why that same caller cannot recover once the exception is thrown.

As a countermeasure, we have Design by Contract proposal. Feature provider makes public methods and properties that can be used to check correctness of the object and arguments to its methods prior to making actual calls. Any consumer which might not be aware of part of the state is then free to make these test calls first, and to quit further work if it would lead to failure.

It is a separate question what the caller can do when it finds that some preconditions would be violated. It makes sense to test and recover from errors only if there is a meaningful recovery procedure. Otherwise, if the caller does not possess knowledge how to recover from certain condition, then it should not test that condition at all, and precondition violation seems to be the only course of action that remains.

Bottom line is that if it ever happens that precondition (or postcondition) gets violated, then that is the indication of a defect in code. There is no reasonable recovery action that could be taken by the application. Instead, it is the programmer who must be informed about the failure and he or she would have to modify code to avoid further failures.

In that respect, be aware that precondition failures indicate that there is a bug in the code which has placed the call. The bug is obviously not within the method which has thrown the precondition exception, simply because that method is the one which has detected the failure condition.

On the other hand, postcondition failure indicates that the defect is located in the method which has thrown it. This is because postconditions are modeling conditions that must hold after the method was executed, and failure indicates that current method (or one called by it) has violated the rules.


If you wish to learn more, please watch my latest video courses

About

Zoran Horvat

Zoran Horvat is the Principal Consultant at Coding Helmet, speaker and author of 100+ articles, and independent trainer on .NET technology stack. He can often be found speaking at conferences and user groups, promoting object-oriented and functional development style and clean coding practices and techniques that improve longevity of complex business applications.

  1. Pluralsight
  2. Udemy
  3. Twitter
  4. YouTube
  5. LinkedIn
  6. GitHub