SoFunction
Updated on 2025-04-04

Summary of thoughts on .NET exception handling

The end of the year is approaching, and for most programmers, they will be free for a while soon. However, in this leisure time, they can only argue about which language is better to spend their time. I guess many blog posts about Java and .net have appeared recently. I said that as a melon-eating crowd, I would quietly watch the big guys express their feelings.

I have said enough nonsense, so I won’t talk nonsense anymore. Let’s get to the point.

In project development, there are corresponding requirements for the stability and fault tolerance of the system and code. The difference between the code in actual development projects and sample code is more about the stability, fault tolerance and scalability of the code operation. Because for implementing a function, the core code for implementing a function is the same, which may be just optimized in writing, but for implementing a class used in a certain operation, this is the same most of the time. In this way, we need to consider many issues in the actual development process. We are no longer limited to the implementation of a specific function, but more about the stability and scalability of the code.

The above are the problems that need to be faced in actual development. In my recent blog post, the author is also considering how to write this exception and how to understand it. I hope it will be helpful to everyone. Everyone is also welcome to put forward their own ideas and opinions and share their knowledge and insights.

1. Overview of DotNET exceptions:

When it comes to abnormalities, we need to know what abnormalities are. If we want to learn everything, we should know what we want to learn, so that we can have a rough understanding in our hearts. An exception refers to an action that a member has not completed its name that claims to be accomplished. In .NET, constructors, obtaining and setting properties, adding and removing events, calling operators overloading and calling conversion operators, etc. have no way to return error codes, but in these constructs, errors need to be reported, so an exception handling mechanism must be provided.

In exception handling, the three blocks we often use are: try block; catch block; finally block. These three blocks can be used together or without a catch block. Exception handling blocks can be used in nesting. The specific method will be introduced below.

In the exception handling mechanism, there are generally three options: retweet the same exception and notify the code on the call stack that the exception occurs; throw a different exception and want the code on the call stack to provide richer exception information; let the thread exit from the bottom of the catch block.

There are some guiding suggestions on how to handle exceptions.

1. Use finally block appropriately:

The finally block can ensure that no matter what type of exception is thrown by the thread, it can be executed. The final block is generally used to clean up operations that have been successfully started, and then return to the code after the caller or finally block.

2. Exception capture needs to be appropriate:

Why should exceptions be caught appropriately? The following code is because we cannot catch all exceptions. After catching exceptions, we need to deal with these exceptions. If we catch all exceptions but do not foresee the exceptions, we will not be able to deal with these exceptions.

If the application code throws an exception, the other end of the application may expect to catch the exception, so it cannot be written as a "size-take-all" exception block. The exception should be allowed to move upwards in the call stack, allowing the application code to handle the exception in a targeted manner.

In catch blocks, you can use catch exceptions, but it is best to re-throw the exception at the end of the catch block. As for the reasons, we will explain later.

   try
   {
    var hkml = GetRegistryKey(rootKey);
    var subkey = (subKey);
    if (subkey != null && keyName != )
     (keyName, keyValue, );
   }
   catch (Exception ex)
   {
    ("Create registry error" + ex);
    throw new Exception(,ex);
   }

3. Recover from exception:

After catching the exception, we can write some exception recovery code in a targeted manner to keep the program running. When catching exceptions, you need to catch specific exceptions, fully grasp the situation under which exceptions will be thrown, and know which types are derived from the captured exception types. Do not handle or catch the exception unless the exception is rethrown at the end of the catch block.

4. Maintain the status:

Generally speaking, when we complete an operation or a method, we need to call several methods to complete the process. During the execution process, the previous methods will be completed, and an exception will occur in the subsequent methods. The operation completed by rolling back partly when an unrecoverable exception occurs, because we need to recover information, and when we catch exceptions, we need to catch all exception information.

5. Hide implementation details to maintain the contract:

Sometimes it may be necessary to catch an exception and re-throw a different exception, so that the contract of the method can be maintained, and the thrown heart exception type should be a specific exception. Look at the following code:

FileStream fs = null;
   try
   {
    fs = FileStream();
    
   }
   catch (FileNotFoundException e)
   {
// Throw a different exception, include the exception information, and set the original exception to an internal exception    throw new NameNotFoundException();
   }
   catch (IOException e)
   {
 
    // Throw a different exception, include the exception information, and set the original exception to an internal exception    throw new NameNotFoundException(); 
   } 
   finally 
   {
    if (fs != null) 
    { 
    (); 
   } 
   }

The above code is just an explanation of a way to deal with it. All exceptions thrown should be passed up along the method's call stack, rather than throwing a new exception after "swallowing" them. If a type constructor throws an exception and the exception is not caught in the type constructor method, the CLR will catch the exception internally and throw a new TypeInitialztion instead.

2. Commonly used processing mechanisms for DotNET exceptions:

After an exception occurs in the code, we need to handle the exception. If an exception is not processed in time, the CLR will terminate the process. In exception handling, we can catch the exception in one thread and re-throw the exception in another thread. When an exception is thrown, the CLR will look up the catch block in the call stack that matches the thrown exception type. If no catch block matches the thrown exception type, an unhandled exception occurs. If the CLR detects that any thread in the process has a bit processing exception, it will terminate the process.

1.Exception handling block:

(1).try block: Included code usually requires some common resource cleaning operations, or needs to be recovered from exceptions, or both. The try block can also contain code that may throw exceptions. A try block has at least one associated catch block or final block.

(2).catch block: contains code that needs to be executed in response to an exception. The expression in parentheses after the catch keyword is the capture type. The capture type is specified from or its derived class. CLR searches for a matching catch block from top to bottom, so specific exceptions should be taught to be placed on top. Once the CLR finds a catch block with a matching capture type, the code in all finally blocks of the inner layer is executed. "Inner finally" refers to all finally blocks between the tey blocks that throw the exception and the catch blocks that match the exception.

After catching exceptions, you can re-throw the exception at the end of the catch block, because if we do not handle or terminate the program in time after catching the Exception exception, this exception may pose a great security risk to the program. The Exception class is the base class of all exceptions and can catch all exceptions in the program. If a large exception occurs, we do not handle it in time, which will cause huge problems.

(3). finally block: The code contained is code that is guaranteed to be executed. After all code of the finally block is executed, the thread exits the finally block and executes the statement immediately after the finally block. If there is no finally block, the thread will start executing from the statement after the last catch block.

Note: Exception blocks can be combined and nested. For examples of three exception blocks, I will not introduce them here. Nesting of exceptions can prevent unhandled exceptions from appearing again when handling exceptions. I will not repeat the above.

2.Exception handling example:

(1).Exception processing extension method:

  /// <summary>
  /// Format exception message  /// </summary>
  /// <param name="e">Exception object</param>  /// <param name="isHideStackTrace">Whether to hide exception scale information</param>  /// <returns>Formatted exception information string</returns>  public static string FormatMessage(this Exception e, bool isHideStackTrace = false)
  {
   var sb = new StringBuilder();
   var count = 0;
   var appString = ;
   while (e != null)
   {
    if (count &gt; 0)
    {
     appString += " ";
    }
    (("{0}Exception message:{1}", appString, ));
    (("{0}Exception type:{1}", appString, ().FullName));
    (("{0}Exception method:{1}", appString, ( == null ? null : )));
    (("{0}Exception source:{1}", appString, ));
    if (!isHideStackTrace &amp;&amp;  != null)
    {
     (("{0}Exception stack:{1}", appString, ));
    }
    if ( != null)
    {
     (("{0}Internal exception:", appString));
     count++;
    }
    e = ;
   }
   return ();
  }

(2). Verification exception:

  /// &lt;summary&gt;
  /// Check that the string is empty or empty and throw an exception  /// &lt;/summary&gt;
  /// <param name="val">value test</param>  /// <param name="paramName">param check name</param>  public static void CheckNullOrEmpty(string val, string paramName)
  {
   if ((val))
    throw new ArgumentNullException(paramName, "Value can't be null or empty");
  }

  /// &lt;summary&gt;
  /// Please check that the parameters are not empty or empty and throw an exception  /// &lt;/summary&gt;
  /// <param name="param">Check value</param>  /// <param name="paramName">param name</param>  public static void CheckNullParam(string param, string paramName)
  {
   if ((param))
    throw new ArgumentNullException(paramName, paramName + " can't be neither null nor empty");
  }

  /// &lt;summary&gt;
  /// Check that the parameter is not invalid and throw an exception  /// &lt;/summary&gt;
  /// <param name="param">Check value</param>  /// <param name="paramName">param name</param>  public static void CheckNullParam(object param, string paramName)
  {
   if (param == null)
    throw new ArgumentNullException(paramName, paramName + " can't be null");
  }

  /// &lt;summary&gt;
  /// Please check that parameter 1 is different from parameter 2  /// &lt;/summary&gt;
  /// <param name="param1">Value 1 test</param>  /// &lt;param name="param1Name"&gt;name of value 1&lt;/param&gt;
  /// &lt;param name="param2"&gt;value 2 to test&lt;/param&gt;
  /// &lt;param name="param2Name"&gt;name of vlaue 2&lt;/param&gt;
  public static void CheckDifferentsParams(object param1, string param1Name, object param2, string param2Name)
  {
   if (param1 == param2) {
    throw new ArgumentException(param1Name + " can't be the same as " + param2Name,
     param1Name + " and " + param2Name);
   }
  }

  /// &lt;summary&gt;
  /// Check that an integer value is positive (0 or greater)  /// &lt;/summary&gt;
  /// <param name="val">Integer Test</param>  public static void PositiveValue(int val)
  {
   if (val &lt; 0)
    throw new ArgumentException("The value must be greater than or equal to 0.");
  }

(3).Try-Catch extension operation:

  /// &lt;summary&gt;
  /// Execute specified functions and subsequent functions on an object, and handle exceptions  /// &lt;/summary&gt;
  /// <typeparam name="T">Object type</typeparam>  /// <param name="source">value</param>  /// <param name="action">Main function code to execute on the value</param>  /// <param name="failureAction">Function code in catch</param>  /// <param name="successAction">Function code executed after the main function code is successful</param>  /// <returns>Is the main function code executed smoothly</returns>  public static bool TryCatch&lt;T&gt;(this T source, Action&lt;T&gt; action, Action&lt;Exception&gt; failureAction,
   Action&lt;T&gt; successAction) where T : class
  {
   bool result;
   try
   {
    action(source);
    successAction(source);
    result = true;
   }
   catch (Exception obj)
   {
    failureAction(obj);
    result = false;
   }
   return result;
  }

  /// &lt;summary&gt;
  /// Execute a specified function on an object and handle exceptions  /// &lt;/summary&gt;
  /// <typeparam name="T">Object type</typeparam>  /// <param name="source">value</param>  /// <param name="action">Main function code to execute on the value</param>  /// <param name="failureAction">Function code in catch</param>  /// <returns>Is the main function code executed smoothly</returns>  public static bool TryCatch&lt;T&gt;(this T source, Action&lt;T&gt; action, Action&lt;Exception&gt; failureAction) where T : class
  {
   return (action,
    failureAction,
    obj =&gt; { });
  }

  /// &lt;summary&gt;
  /// Execute a specified function on an object and handle exceptions and return values  /// &lt;/summary&gt;
  /// <typeparam name="T">Object type</typeparam>  /// <typeparam name="TResult">Return value type</typeparam>  /// <param name="source">value</param>  /// <param name="func">Main function code to execute on the value</param>  /// <param name="failureAction">Function code in catch</param>  /// <param name="successAction">Function code executed after the main function code is successful</param>  /// <returns>Return the return value of the function code. If an exception occurs, the default value of the object type</returns>  public static TResult TryCatch&lt;T, TResult&gt;(this T source, Func&lt;T, TResult&gt; func, Action&lt;Exception&gt; failureAction,
   Action&lt;T&gt; successAction)
   where T : class
  {
   TResult result;
   try
   {
    var u = func(source);
    successAction(source);
    result = u;
   }
   catch (Exception obj)
   {
    failureAction(obj);
    result = default(TResult);
   }
   return result;
  }

  /// &lt;summary&gt;
  /// Execute a specified function on an object and handle exceptions and return values  /// &lt;/summary&gt;
  /// <typeparam name="T">Object type</typeparam>  /// <typeparam name="TResult">Return value type</typeparam>  /// <param name="source">value</param>  /// <param name="func">Main function code to execute on the value</param>  /// <param name="failureAction">Function code in catch</param>  /// <returns>Return the return value of the function code. If an exception occurs, the default value of the object type</returns>  public static TResult TryCatch&lt;T, TResult&gt;(this T source, Func&lt;T, TResult&gt; func, Action&lt;Exception&gt; failureAction)
   where T : class
  {
   return (func,
    failureAction,
    obj =&gt; { });
  }

This article does not specifically introduce the use of try, catch, and finally, but gives some more general methods. The main reason is that ordinary developers have a clear understanding of the use of three blocks, so they will not repeat the introduction.

3.DotNET's Exception class analysis:

CLR allows exceptions to throw instances of any type. Here we introduce a class:

Attribute: Indicates the reason for throwing the exception.

[__DynamicallyInvokable]
public virtual string Message
{
 [__DynamicallyInvokable]
 get
 {
  if (this._message != null)
  {
   return this._message;
  }
  if (this._className == null)
  {
   this._className = ();
  }
  return ("Exception_WasThrown", new object[] { this._className });
 }
}

From the above code, we can see that Message only has a get attribute, so message is a read-only attribute. GetClassName() gets the exception class. GetRuntimeResourceString() gets the runtime resource string.

Properties: Contains the names and signatures of all methods called before the exception is thrown.

public static string StackTrace
{
 [SecuritySafeCritical]
 get
 {
  new EnvironmentPermission().Demand();
  return GetStackTrace(null, true);
 }
}

EnvironmentPermission() is used to restrict environments, set permission status, and GetStackTrace() gets stack trace. Let's take a look at the code of GetStackTrace() in detail.

internal static string GetStackTrace(Exception e, bool needFileInfo)
{
 StackTrace trace;
 if (e == null)
 {
  trace = new StackTrace(needFileInfo);
 }
 else
 {
  trace = new StackTrace(e, needFileInfo);
 }
 return ();
}
public StackTrace(Exception e, bool fNeedFileInfo)
{
 if (e == null)
 {
  throw new ArgumentNullException("e");
 }
 this.m_iNumOfFrames = 0;
 this.m_iMethodsToSkip = 0;
 (0, fNeedFileInfo, null, e);
}

The above is the specific implementation of the method to obtain the stack trace, which is mainly used when users debug.

() Method for obtaining basic exception information.

[__DynamicallyInvokable]
public virtual Exception GetBaseException()
{
 Exception innerException = ;
 Exception exception2 = this;
 while (innerException != null)
 {
  exception2 = innerException;
  innerException = ;
 }
 return exception2;
}

The InnerException property is an intrinsic exception, which is a virtual method and is rewritten here. Take a look at the InnerException property in detail.

[__DynamicallyInvokable]
public Exception InnerException
{
 [__DynamicallyInvokable, TargetedPatchingOptOut("Performance critical to inline this type of method across NGen image boundaries")]
 get
 {
  return this._innerException;
 }
}

() Format the exception information.

private string ToString(bool needFileLineInfo, bool needMessage)
{
 string className;
 string str = needMessage ?  : null;
 if ((str == null) || ( <= 0))
 {
  className = ();
 }
 else
 {
  className = () + ": " + str;
 }
 if (this._innerException != null)
 {
  className = className + " ---> " + this._innerException.ToString(needFileLineInfo, needMessage) +  + " " + ("Exception_EndOfInnerExceptionStack");
 }
 string stackTrace = (needFileLineInfo);
 if (stackTrace != null)
 {
  className = className +  + stackTrace;
 }
 return className;
}

In this method, the obtained exception information is formatted into a string, and () obtains the relevant information of the exception class.

As above, we noticed the [__DynamicallyInvokable] custom attribute, let's take a look at the specific implementation code:

[TargetedPatchingOptOut("Performance critical to inline this type of method across NGen image boundaries")]
public __DynamicallyInvokableAttribute()
{
}

For the above main annotation part, please refer to "Via CLR c#" for the relevant information on the attribute "Image Boundary". We will not introduce it in detail here.

4. Summary:

In the above introduction to exceptions, we mainly introduce the exception handling mechanism of CLR, some more general exception codes, and the introduction of Exception class. In actual projects, we generally do not throw exceptions directly to the customer. When writing the program, we have considered the fault tolerance of the program. After the program catches the exception, try to restore the program, or write exception information to the log to let the program enter the error page. If a serious exception occurs, the exception will be thrown and the program will be terminated.

I hope it will be helpful to everyone's learning and I hope everyone will support me more.