Use argument exceptions

Back to articles.

An exception might just be a side effect of a real error. For instance, if method arguments are not validated, it might lead to invalid data being saved into the database. That data inconsistency will go unnoticed for some time until another part of the code base tries to use that data, resulting in another error. Those errors are much harder to reproduce since you can see the invalid data but not why it's there.

Argument exceptions are a low effort tool used to discover errors early. Catching errors earlier means that the error is closer to the root cause, like invalid data being entered by the user. Being closer to the root cause will save valuable debugging time and it's more likely that the real error is corrected instead of doing a workaround.

In the essence, to get started using argument exceptions, add sanity checks to the beginning of your method:

public void ApplyDiscount(int productId, decimal discountPercentage)
{
    if (productId <= 0)
        throw new ArgumentOutOfRangeException(nameof(productId), productId,
            "A valid product id must be specified.");
    if (discountPercentage <= 1 || discountPercentage > 100)
        throw new ArgumentOutOfRangeException(nameof(discountPercentage), discountPercentage,
            "A valid discount must be specified.");

    // [... rest of the code .. ]
}

At a first glance, those checks look just like code clutter and not really useful. Won't productId always be valid since it's typically specified by code and not by the user? Isn't it obvious that the discount should be between 1 and 100?

Let's examine those arguments further.

productId

The productId can actually contain an invalid value in many different scenarios. Here are two examples.

When using mappers

Are you using AutoMapper, ValueInjecter or similar libraries? What they typically do is do is copying information in one class to another class.

Something like this:

var businessEntity = Mapper.Map<DiscountViewModel, DiscountEntity>(viewModel);

Normally that isn't a problem. But all applications grow. A year later one of those classes might have been refactored. The DiscountPercentage field might only be called Discount.

That is a problem since mappers typically do not throw exceptions if a property mapping is missing (since the two classes typically have different sets of properties).

Invoking the ApplyDiscount of DiscountEntity might work the first year in the applications lifetime, but then start to fail due to a refactoring. Trying to nail that down to an invalid mapping can be a headache.

When writing web applications

Web applications typically have both client-side and server-side code, which means that the application has conversions from dynamic languages to typed languages.

Fields can be changed at one side and not the other which also leads to mapping issues.

It's also easy to misspell names or do accidental renames. Something that I've done several times is pressing just 'S' instead of CTRL+S when wanting to save the document (i.e. pressing CTRL to late/early). That can lead to a input rename, <input type="number" name="dsiscount">, in your client side code.

discountPercentage

When programming, percentage calculations are commonly done in decimal form. To apply a 20% discount you do something like price * 0.8.

You therefore have three different ways to specify the discount:

  • 0.2, 20% in decimal form (price * (1-discount))
  • 0.8, what you should apply to the price (price * discount)
  • 20, the discount in percentage (price * (1.0 - (discount / 100)))

Some of them might look stupid to you, but can you promise that all who will ever code in your application will think exactly like you?

If not, sooner or later someone will use one of the forms that you did not expect, resulting in calculation errors.

Why does it matter?

For the discount it's obvious, the discount calculation will go terribly wrong if someone uses the decimal version of a discount (0.5 instead of 50%).

Is it so bad if we don't validate the product id? Won't we get an exception either way, since the product 0 will not be found?

Yes, you will, but then the exception has many reasons to be thrown.

With ArgumentOutOfRangeException you get the exception message:

A valid product id must be specified. Parameter name: productId, Actual value was 0.

A database exception would state something like:

Entity not found.

The entity could have been deleted, an invalid reference could have been stored somewhere or the method argument is incorrect.

Thus, when you use argument exceptions you now that the Entity not found exceptions is for data errors while the argument exception typically is for mapping/conversion errors.

Conclusion

Argument exceptions are typically sanity checks. They are used to guard against parse errors, invalid logic (another function delivered the wrong result), incorrect measuring units and simple typos.

Using argument exceptions also means that you will discover errors earlier and get meaningful exception messages that makes it easier to figure out what failed.

Call to action

The following practices will help you write robust applications that are easier to debug.

  • Do sanity checks, at minimum in the business layer.
  • Include the value that failed in the thrown exception.
  • Use meaningful exception messages that clearly states why something went wrong.