What is an exceptional situation in code?

This is a continuation of the topic of error handling. We’ll discuss what an exceptional situation in code actually is and see some examples of it.

Exceptional situations

I like to use the guideline "use exceptions for exceptional situations". It emphasizes that exceptions should be employed sparingly, and not as a tool for every job. However, I never stated what those exceptional situations really are, and haven’t seen anyone else doing this either.

Kirk Larkin rightfully pointed out in the comments to my previous post that without further elaboration, this guideline is quite vague and subjective. And it indeed is. So here, I’ll try to bring the exhaustive list of exceptional situations as well as a list of cases which shouldn’t be treated as such (but usually are). If you find something you disagree with or think I missed, feel free to write a comment below, I’ll update the post accordingly.

Bugs

The first type of exceptional situations is bugs. Of course, in most cases, you don’t know if your code contains a bug. You sort of assume that it doesn’t but can’t know for sure. And if you do know about some issue, that knowledge usually leads to fixing that issue right away, so it doesn’t live there for long. So how can you use exceptions to represent bugs in your code if you are not aware of them in the first place?

Well, there are cases where you can do that. The most common example of it is when some of the code paths in your program are illegal. Below is an example:

public enum ShapeType
{
    Circle,
    Square
}
 
public void DrawShape(ShapeType shapeType)
{
    switch (shapeType)
    {
        case ShapeType.Circle:
            DrawCircle();
            break;
 
        case ShapeType.Square:
            DrawSquare();
            break;
    }
 
    // Should not get here
    throw new InvalidOperationException("Unknown shape type: " + shapeType);
}

Here, only the first two of the three code branches are valid: the method can do something meaningful only if the shape type is either Circle or Square. While the program could theoretically take the 3rd path and not hit any of the cases in the switch statement, if it does, it means we added a new type but missed a corresponding handler for it. In other words, introduced a bug. Such a bug is an exceptional situation and should be treated accordingly - by throwing an exception.

This is another example:

public void SetDiscount(CustomerType customerType, int? discount)
{
    if (customerType == CustomerType.Regular && discount != null)
        throw new InvalidOperationException("Invalid customer type");
 
    /* Set the discount */
}

Here, the discount can only apply if the customer’s type is not Regular. The situation where we try to set a discount to a regular customer is illegal. If it happens, it means we’ve messed up and introduced a bug to the client of this method.

That’s basically it. This type of exceptional situations - a Bug - is any situation that can happen in theory but shouldn’t take place in practice. They are illegal.

To some degree, most of such situations are the result of limitations imposed by programming languages we use. For example, the illegal case in the switch statement could be mitigated should C# had a proper pattern matching and the compiler pointing out non-exhaustive matches. And the illegal discount could be taken care of should C# introduced discriminated unions / sum types.

The general guideline here is that you need to make illegal states unrepresentable by using the type system. So that you couldn’t even compile such code. Or at least have the compiler give you a warning. It’s not always possible of course, hence the use of exceptions to protect yourself against possible bugs. Here I wrote more about ways to achieve code correctness: unit tests, assertions, and type system.

Design by contract

Another type of exceptional situations come into play when you practice contract programming. The general idea here is that each class or method have its own set of invariants, preconditions, and postconditions.

Invariants are conditions that are held true during the whole lifetime of a class. It’s basically some characteristic of the class you can rely upon at all times. For example, in this code:

public class Customer
{
    public CustomerType Type { get; private set; }
    public int? Discount { get; private set; }
}

the invariant might be that, as I mentioned earlier, customers of Regular type should not have any discounts. Customer instances violating this invariant may not be created.

Unfortunately, in C#, there’s no way to specify the invariants out of the box, using only the language itself. You could use the Code Contracts library but it requires some work to include it into the build pipeline.

The ideal solution would be to have a declarative syntax like this:

public class Customer
    invariant Type != CustomerType.Regular || Discount == null
{
    public CustomerType Type { get; private set; }
    public int? Discount { get; private set; } 
 
    public void Update(decimal moneySpentThisYear)
    {
        Type = CalculateType(moneySpentThisYear);
        Discount = CalculateDiscount(moneySpentThisYear);
    } 
}

The compiler then would check this invariant at the beginning and at the end of each public method or property and throw an exception if it’s violated. Unfortunately, this is not on the radar of the C# team for now.

You could still introduce such checks manually but of course, they wouldn’t look as cool as in the above code :) A hand-written version can look like so:

public class Customer
{
    public CustomerType Type { get; private set; }
    public int? Discount { get; private set; }
 
    public void Update(decimal moneySpentThisYear)
    {
        Type = CalculateType(moneySpentThisYear);
        Discount = CalculateDiscount(moneySpentThisYear);
 
        if (Type == CustomerType.Regular && Discount != null)
            throw new InvalidOperationException("Invalid type and discount");
    }
}

Preconditions and postconditions are similar to invariants with the exception that they only valid for a particular method. A precondition is some obligation the client code must meet before calling that method. And a postcondition is some guarantee the method itself is ought to fulfill by the time it ends executing.

Violating any of these three sets of obligations means that something has fallen apart and the code no longer works as expected. Which is another way to say that there’s a bug in it.

Yeah, that’s right, contract violation is very similar to the previous type of exceptional situations as it also denotes a bug. But I still wanted to put it into its own category as this programming concept is so widely known.

Failed assumptions

Another type is failed assumptions. Such assumptions usually take place when you interact with a 3rd party service and expect responses from it to have some specific structure.

For example, if you integrate with Facebook and Google on your login page via OAuth, you might reasonably assume that whenever you query a social provider with a user token, it will return back the user info which has at least one piece of information - user email. You can tolerate the lack of other data, such as first or last name, but without an email, you can’t proceed with registering the user. Reading the documentation and testing the OAuth integration with a couple of fake accounts confirms your belief and so you are pretty sure the user email will always be there.

But what if it’s not?

This information is crucial for this particular operation. You can say that it’s a fundamental assumption that the social network will always provide this data. A single instance of user info not containing an email would render this assumption false. It would mean you need to rethink the whole registration workflow.

The best way to ensure your assumptions are honored is to perform assertions whenever you accept data from external sources. Check that the incoming user email is valid. And if it is not, throw an exception. It would signalize a fundamental flaw in the registration process and will allow you to quickly notice it.

Unrecoverable hardware or software failures

Another type of exceptional situations is unrecoverable failures of the software or hardware used in your system. What if you write an API and that API can’t access the database due to a network failure? Without this database, it can’t perform the operation it was asked to do, and so the only possible way out here is to throw an exception thereby stating a bankruptcy.

Or what if the API needs to save a file posted by the user to the disk and this disk fails to accept the write? You can’t do anything about this failure either, so throw an exception and call it a day.

Non-exceptional situations: validation

Let’s now talk about situations that shouldn’t be considered exceptional (and thus shouldn’t be treated with an exception).

The first and the most common one is validation failures. If you accept an input from a client and there’s something wrong with it, that’s a perfectly valid situation from the business standpoint. You just need to politely reject that input and state what exactly the client did wrong.

Did it request an account info by an id which is not present in the database? Nope, not gonna get this account. Did it try to purchase a toaster but the provided credit card number is invalid? Nope, not gonna get this toaster either.

As I mentioned in the previous post, the best way to deal with non-exceptional situations is to be explicit about the program flow and represent the result of the validation with a Result instance. Exceptions will only make it harder to understand that flow.

Note that in the case of exceptional situations we talked about before, exceptions will not make the execution flow harder to read because they represent the moment where this flow ends. There’s just nothing to trace beyond that termination point.

Transient or recoverable hardware and software failures

Another type of non-exceptional situations is hardware and software failures you can work around or recover from. The most common example here is a re-try logic you put to overcome sporadic network failures when accessing an external service.

A recoverable failure would be a failure whose occurrence is expected by your workflow. For example, an email client that can’t connect to the SMPT server. Such connection issue is a failure but it is expected: the user will see a corresponding error message which will go away once the connection is restored. No one would use a client that works only when connected to the Internet and crashes otherwise.

Migration between the two categories

It’s important to note that a situation can migrate from one category to the other.

For example, you may think that a 3rd party service you get some data from is reliable. You assume it is always online and implicitly treat any failure in it as fatal.

After getting a couple of such fatal errors, you discover that those errors are transient and so you put a re-try logic. It raises a fatal failure only if you can’t reach the service for a number of times straight. This re-try logic fixes the issue.

What you did here is you transitioned this failure from the exceptional category to the non-exceptional one. You now know how to deal with it.

An opposite example is when you realize that an invariant you had in code for some class no longer reflects its nature and thus can be removed. By deleting this invariant, you enable states that were previously illegal. In other words, you make formerly exceptional situations non-exceptional.

Summary

Exceptional situations are failures you can’t recover from programmatically. They are:

  • Illegal code paths or states whose presence means you introduced a bug to your software.

  • Contract violation: violation of an invariant, pre- or postcondition.

  • Failed assumptions: assumptions you make about external services that are rendered to be false.

  • Unrecoverable software and hardware failures.

Non-exceptional situations are failures you can recover from programmatically or even expect to happen. They are:

  • Failed input validations.

  • Transient or recoverable software and hardware failures.

Subscribe


I don't post everything on my blog. Don't miss smaller tips and updates. Sign up to my mailing list below.

Comments


comments powered by Disqus