SoFunction
Updated on 2025-03-07

Analyze the application methods of assertions and exceptions in C# and the process control of exception handling

Assertion Vs Exception
In daily programming practice, the boundaries between assertions and exceptions are not very obvious, which makes them often not used correctly. I am also constantly fighting this vague monster. I will only write this article to share my personal opinions with you. I think we can also distinguish between assertions and exceptions from many perspectives. Everyone's opinions and suggestions are welcome.

Exception usage scenario: used to catch possible external errors

Assertion usage scenario: used to capture internal impossible errors

We can first carefully analyze the exceptions we already have in .net.

  • SqlException
  • IOException
  • ServerException

First, let's not regard them as exceptions, because we have not drawn a clear line between exceptions and assertions yet, let's regard them as errors first.

When we consider the possible file loading errors or server errors in the first field of encoding, our first instinct is that this is not a problem with our code, it is a problem outside of our code.

For example, the following code

public void WriteSnapShot(string fileName, IEnumerable<DbItem> items)
    {
      string format = "{0}\t{1}\t{2}\t{3}\t{4}\t{5}";
      using (FileStream fs = new FileStream(fileName, ))
      {
        using (StreamWriter sw = new StreamWriter(fs, ))
        {
 ...
          foreach (var item in items)
          {
            ((format, new object[]{
              ,
              ,
              ,
              ,
              ,
              }));
          }
          ();
        }
      }
    }

The above code is writing to the file, which obviously leads to IOException. A little experienced programmers will consider that there may be problems in IO, so how should we deal with this problem? In this context, we have no other way, we can only let the error continue to be thrown up and notify the caller of the above layer that an error occurred. As for how the caller of the previous layer will handle it, it is not the problem that this function needs to consider. But in this function, remember to free up the resources occupied by the current function. Therefore, when an external error that we cannot control occurs, we can throw it up as an exception. At this time, we should use the exception.

Now let’s look at the assertions, and we will use the following code as an example.

public  GetSimpleBugInfo(string bugNum)
    {

      var selector = <ISelector>();

      var list = <>(
        reader => new 
        {
          bugNum = reader["bugNum"].ToString(),
          dealMan = reader["dealMan"].ToString(),
          description = reader["description"].ToString(),
          size = Convert.ToInt32(reader["size"]),
          fired = Convert.ToInt32(reader["fired"]),
        },
        "select * from bugInfo",
        new WhereClause(bugNum, "bugNum"));

      (list != null);
      
      if ( == 0)
        return null;
      else
        return list[0];

    }

When I posted this code, I felt a little bumpy because I had been struggling here for a long time, which is one of the reasons why I have never drawn the line between assertions and exceptions.

First, let’s review the assertion usage scenario defined earlier: errors that cannot occur internally.

Is this code internal code? If we can modify the code in Return, it means it is internal code; otherwise, it means it is external code. For internal code, we can use assertions to protect the invariance of its logic. When the assertion is triggered, we can be sure that it is an error in the internal code and we should fix it immediately.

Let's worry about it, assuming that Return is external code, we have no way to modify it. Then there are two ways to write the above code (if you have more ideas, please give me some advice).

The first type is to directly throw an exception.

If(list == null)
{
  throw new NullReferenceException();
}

The second type is to adjust the code.

if(list == null ||  == 0)
{
  return null;
}
else
{
  return list[0];
}

Of course, there is another way to do nothing and let the code execute until the system throws an empty reference error for you. However, this practice violates the principle of anti-discharge programming. We should always handle errors as soon as possible or nearest to the place where the error occurred, so as to avoid the error data flowing to other parts of the system and causing more serious errors.

Summarize

The use of exceptions or assertions depends on whether you want to prevent an internal error or an external error and what you think is an internal error or an external error. If you decide to prevent an internal error, please use assertion decisively, otherwise, use exceptions.

Exception handling
Exception handling controls the process, just like throwing and capturing are divided into two aspects:

If an error (or some situation) occurs, is it allowed to continue to execute the control flow of the program (exception throwing)
If an exception occurs, does the current code have a chance to let the program's control flow enter a reasonable state (exception capture)
I think the above two can be used as the criteria for judging exception handling. In fact, you should now find that the so-called standard focuses on the impact of exceptions on the process, and no longer under what circumstances does exceptions use.

The most direct thing about process control is the following code

try
          {
            foreach (var lockGroup in lockGroups)
            {
 
              ...
 
              foreach (var newlock in ())
              {
                ...
                if ((n => (, )))
                {
                  status = ;
                  throw new LockException();
                }
 
                var diningAvail = (n =>  == );
                if (diningAvail == null)
                {
                  status = ;
                  throw new LockException();
                }
 
                ...
                 
                if (newLockQuantity >  && !)
                {
                  status = ;
                  throw new LockException();
                }
                else if (newLockQuantity + reservedQuantity + currentLockedAvail >  && !)
                {
                  status = ;
                  throw new LockException();
                }
 
                ...
              }
            }
          }
          catch (LockException)
          {
            return new DiningLock[] { };
          }

In the above code, there are two layers of for loops. When something happens in the innermost layer, it is required to stop the execution of the entire for loop. Obviously, using two breaks is not possible, and an auxiliary variable must be added.

However, if you use exceptions, this processing will be much simpler. Exceptions can be thrown directly at the innermost layer and caught at the outermost layer (or where process control is required).

In the above code, exception handling plays a role in process control, rather than just passing error information, and contributes to the simplification of the code.