Search code examples
c#debuggingexceptionassert

Appropriate usage of assertions and exceptions


I've read a bit around, trying to figure out when to use assertions and exceptions appropriately, but there's still something I'm missing to the big picture. Probably I just need more experience, so I'd like to bring some simple examples to better comprehend in what situations I should use use what.

Example 1: let us start with the classical situation of an invalid value. For example I have the following class, in which both fields must be positive:

class Rectangle{
    private int height;
    private int length;

    public int Height{
        get => height;
        set{
            //avoid to put negative heights
        }
    }
    //same thing for length
}

Let me remark that I am not talking about how to deal with user input in this example, since I could just make a simple control flow for that. Though, I am facing with the idea that somewhere else there may be some unexpected error and I want this to be detected since I don't want an object with invald values. So I can:

  • Use Debug.Assert and just stop the program if that happens, so that I can correct possible errors when they come.
  • Throw an ArgumentOutOfRangeException to basically do the same thing? This feels wrong, so I should use that only if I know I'm going to handle it somewhere. Though, if I know where to handle the exception, shouldn't I fix the problem where it lies? Or maybe it is meant for things that may happen but you cannot control directly in your code, like an user input (well that can be dealt with, without an exception, but maybe something else can't) or loading data?

Question: did I get the meaning of assertions and exceptions right? Also, could you please give an example in which handling exceptions can be useful (because something you cannot control before happens)? I cannot figure out what else, beyond the cases I mentioned, can happen, but I clearly still lack experience here.
To expand a bit my question: I can think of a variety of reasons why an exception can be thrown, like a NullReferenceException, an IndexOutOfBoundsException, the IO exceptions like DirectoryNotFoundException or FileNotFoundException, etc. Though, I cannot figure out situations in which handling them becomes useful, apart from simply stopping the program (in which case, shouldn't an assertion be used?) or giving a simple message of where the problem has occured. I know even this is useful and exceptions are also meant to categorize "errors" and give clue to how to fix them. Though, is a simple message really all they are useful to? That's sounds fishy, so I'll stick with the "I've never faced a proper situation, 'cause of experience" mantra.

Example 2: let us now talk about the user input, using the first example. As I've anticipated, I won't use an exception just to check that the values are positive, as that's a simple control flow. But what happens if the user inputs a letter? Should I handle an exception here (maybe a simple ArgumentException) and give a message in the catch block? Or it can be avoided, too, through control flow (check if input is of type int, or something like that)?

Thanks to anyone who will clear my lingering doubts.


Solution

  • Throw an ArgumentOutOfRangeException to basically do the same thing? This feels wrong, so I should use that only if I know I'm going to handle it somewhere. Though, if I know where to handle the exception, shouldn't I fix the problem where it lies?

    Your reasoning is pretty good here, but not quite right. The reason you're struggling is because exceptions are used for four things in C#:

    • boneheaded exceptions. A boneheaded exception is something like "invalid argument" when the caller could have known that the argument is invalid. If a boneheaded exception is thrown then the caller has a bug that should be fixed. You never have a catch(InvalidArgumentException) outside of a test case because it should never be thrown in production. These exceptions exist to help your callers write correct code by telling them very loudly when they've made a mistake.

    • vexing exceptions are boneheaded exceptions where the caller cannot know that the argument is invalid. These are design flaws in APIs and should be eliminated. They require you to wrap API calls with try-catches to catch what looks like an exception that should be avoided, not caught. If you find that you're writing APIs that require the caller to wrap calls in a try-catch, you're doing something wrong.

    • fatal exceptions are exceptions like thread aborted, out of memory, and so on. Something terrible has happened and the process cannot continue. There is very little point in catching these because there's not much you can do to improve the situation, and you might make it worse.

    • exogenous exceptions are things like "the network cable is unplugged". You expected the network cable to be plugged in; it is not, and there is no way you could have checked earlier to see if it was, because the time of checking and the time of using are different times; the cable could be unplugged between those two times. You have to catch these.

    Now that you know what the four kinds of exceptions are, you can see what the difference is between an exception and an assertion.

    An assertion is something that must logically be true always, and if it is not, then you have a bug that should be fixed. You never assert that the network cable is plugged in. You never assert that a caller-supplied value is not null. There should never be a test case that causes an assertion to fire; if there is, then the test case has discovered a bug.

    You assert that after your in-place sort algorithm runs, the smallest element in a non-empty array is at the beginning. There should be no way that can be false, and if there is, you have a bug. So assert that fact.

    A throw by contrast is a statement, and every statement should have a test case which exercises it. "This API throws when passed null by a buggy caller" is part of its contract, and that contract should be testable. If you find you're writing throw statements that have no possible test case that verifies that they throw, consider changing it to an assertion.

    And finally, never pass invalid arguments and then catch a boneheaded exception. If you're dealing with user input, then the UI layer should be verifying that the input is syntactically valid, ie, numbers where numbers are expected. The UI layer should not be passing possibly-unvetted user code to a deeper API and then handling the resulting exception.