Best Practices for exceptions - .NET (2024)

  • Article

A well-designed app handles exceptions and errors to prevent app crashes. This article describes best practices for handling and creating exceptions.

Use try/catch/finally blocks to recover from errors or release resources

Use try/catch blocks around code that can potentially generate an exception, and your code can recover from that exception. In catch blocks, always order exceptions from the most derived to the least derived. All exceptions derive from the Exception class. More derived exceptions aren't handled by a catch clause that's preceded by a catch clause for a base exception class. When your code can't recover from an exception, don't catch that exception. Enable methods further up the call stack to recover if possible.

Clean up resources that are allocated with either using statements or finally blocks. Prefer using statements to automatically clean up resources when exceptions are thrown. Use finally blocks to clean up resources that don't implement IDisposable. Code in a finally clause is almost always executed even when exceptions are thrown.

Handle common conditions without throwing exceptions

For conditions that are likely to occur but might trigger an exception, consider handling them in a way that will avoid the exception. For example, if you try to close a connection that's already closed, you'll get an InvalidOperationException. You can avoid that by using an if statement to check the connection state before trying to close it.

if (conn->State != ConnectionState::Closed){ conn->Close();}
if (conn.State != ConnectionState.Closed){ conn.Close();}
If conn.State <> ConnectionState.Closed Then conn.Close()End IF

If you don't check the connection state before closing, you can catch the InvalidOperationException exception.

try{ conn->Close();}catch (InvalidOperationException^ ex){ Console::WriteLine(ex->GetType()->FullName); Console::WriteLine(ex->Message);}
try{ conn.Close();}catch (InvalidOperationException ex){ Console.WriteLine(ex.GetType().FullName); Console.WriteLine(ex.Message);}
Try conn.Close()Catch ex As InvalidOperationException Console.WriteLine(ex.GetType().FullName) Console.WriteLine(ex.Message)End Try

The method to choose depends on how often you expect the event to occur.

  • Use exception handling if the event doesn't occur often, that is, if the event is truly exceptional and indicates an error, such as an unexpected end-of-file. When you use exception handling, less code is executed in normal conditions.

  • Check for error conditions in code if the event happens routinely and could be considered part of normal execution. When you check for common error conditions, less code is executed because you avoid exceptions.

Design classes so that exceptions can be avoided

A class can provide methods or properties that enable you to avoid making a call that would trigger an exception. For example, a FileStream class provides methods that help determine whether the end of the file has been reached. These methods can be used to avoid the exception that's thrown if you read past the end of the file. The following example shows how to read to the end of a file without triggering an exception:

class FileRead{public: void ReadAll(FileStream^ fileToRead) { // This if statement is optional // as it is very unlikely that // the stream would ever be null. if (fileToRead == nullptr) { throw gcnew System::ArgumentNullException(); } int b; // Set the stream position to the beginning of the file. fileToRead->Seek(0, SeekOrigin::Begin); // Read each byte to the end of the file. for (int i = 0; i < fileToRead->Length; i++) { b = fileToRead->ReadByte(); Console::Write(b.ToString()); // Or do something else with the byte. } }};
class FileRead{ public void ReadAll(FileStream fileToRead) { // This if statement is optional // as it is very unlikely that // the stream would ever be null. if (fileToRead == null) { throw new ArgumentNullException(); } int b; // Set the stream position to the beginning of the file. fileToRead.Seek(0, SeekOrigin.Begin); // Read each byte to the end of the file. for (int i = 0; i < fileToRead.Length; i++) { b = fileToRead.ReadByte(); Console.Write(b.ToString()); // Or do something else with the byte. } }}
Class FileRead Public Sub ReadAll(fileToRead As FileStream) ' This if statement is optional ' as it is very unlikely that ' the stream would ever be null. If fileToRead Is Nothing Then Throw New System.ArgumentNullException() End If Dim b As Integer ' Set the stream position to the beginning of the file. fileToRead.Seek(0, SeekOrigin.Begin) ' Read each byte to the end of the file. For i As Integer = 0 To fileToRead.Length - 1 b = fileToRead.ReadByte() Console.Write(b.ToString()) ' Or do something else with the byte. Next i End SubEnd Class

Another way to avoid exceptions is to return null (or default) for most common error cases instead of throwing an exception. A common error case can be considered a normal flow of control. By returning null (or default) in these cases, you minimize the performance impact to an app.

For value types, whether to use Nullable<T> or default as your error indicator is something to consider for your app. By using Nullable<Guid>, default becomes null instead of Guid.Empty. Sometimes, adding Nullable<T> can make it clearer when a value is present or absent. Other times, adding Nullable<T> can create extra cases to check that aren't necessary and only serve to create potential sources of errors.

Throw exceptions instead of returning an error code

Exceptions ensure that failures don't go unnoticed because the calling code didn't check a return code.

Use the predefined .NET exception types

Introduce a new exception class only when a predefined one doesn't apply. For example:

  • If a property set or method call isn't appropriate given the object's current state, throw an InvalidOperationException exception.
  • If invalid parameters are passed, throw an ArgumentException exception or one of the predefined classes that derive from ArgumentException.

End exception class names with the word Exception

When a custom exception is necessary, name it appropriately and derive it from the Exception class. For example:

public ref class MyFileNotFoundException : public Exception{};
public class MyFileNotFoundException : Exception{}
Public Class MyFileNotFoundException Inherits ExceptionEnd Class

Include three constructors in custom exception classes

Use at least the three common constructors when creating your own exception classes: the parameterless constructor, a constructor that takes a string message, and a constructor that takes a string message and an inner exception.

  • Exception(), which uses default values.
  • Exception(String), which accepts a string message.
  • Exception(String, Exception), which accepts a string message and an inner exception.

For an example, see How to: Create User-Defined Exceptions.

Ensure that exception data is available when code executes remotely

When you create user-defined exceptions, ensure that the metadata for the exceptions is available to code that's executing remotely.

For example, on .NET implementations that support app domains, exceptions might occur across app domains. Suppose app domain A creates app domain B, which executes code that throws an exception. For app domain A to properly catch and handle the exception, it must be able to find the assembly that contains the exception thrown by app domain B. If app domain B throws an exception that is contained in an assembly under its application base, but not under app domain A's application base, app domain A won't be able to find the exception, and the common language runtime will throw a FileNotFoundException exception. To avoid this situation, you can deploy the assembly that contains the exception information in either of two ways:

  • Put the assembly into a common application base shared by both app domains.
  • If the domains don't share a common application base, sign the assembly that contains the exception information with a strong name and deploy the assembly into the global assembly cache.

Use grammatically correct error messages

Write clear sentences and include ending punctuation. Each sentence in the string assigned to the Exception.Message property should end in a period. For example, "The log table has overflowed." would be an appropriate message string.

Include a localized string message in every exception

The error message the user sees is derived from the Exception.Message property of the exception that was thrown, and not from the name of the exception class. Typically, you assign a value to the Exception.Message property by passing the message string to the message argument of an Exception constructor.

For localized applications, you should provide a localized message string for every exception that your application can throw. You use resource files to provide localized error messages. For information on localizing applications and retrieving localized strings, see the following articles:

  • How to: Create user-defined exceptions with localized exception messages
  • Resources in .NET apps
  • System.Resources.ResourceManager

In custom exceptions, provide additional properties as needed

Provide additional properties for an exception (in addition to the custom message string) only when there's a programmatic scenario where the additional information is useful. For example, the FileNotFoundException provides the FileName property.

Place throw statements so that the stack trace will be helpful

The stack trace begins at the statement where the exception is thrown and ends at the catch statement that catches the exception.

Use exception builder methods

It's common for a class to throw the same exception from different places in its implementation. To avoid excessive code, use helper methods that create the exception and return it. For example:

ref class FileReader{private: String^ fileName;public: FileReader(String^ path) { fileName = path; } array<Byte>^ Read(int bytes) { array<Byte>^ results = FileUtils::ReadFromFile(fileName, bytes); if (results == nullptr) { throw NewFileIOException(); } return results; } FileReaderException^ NewFileIOException() { String^ description = "My NewFileIOException Description"; return gcnew FileReaderException(description); }};
class FileReader{ private string fileName; public FileReader(string path) { fileName = path; } public byte[] Read(int bytes) { byte[] results = FileUtils.ReadFromFile(fileName, bytes); if (results == null) { throw NewFileIOException(); } return results; } FileReaderException NewFileIOException() { string description = "My NewFileIOException Description"; return new FileReaderException(description); }}
Class FileReader Private fileName As String Public Sub New(path As String) fileName = path End Sub Public Function Read(bytes As Integer) As Byte() Dim results() As Byte = FileUtils.ReadFromFile(fileName, bytes) If results Is Nothing Throw NewFileIOException() End If Return results End Function Function NewFileIOException() As FileReaderException Dim description As String = "My NewFileIOException Description" Return New FileReaderException(description) End FunctionEnd Class

In some cases, it's more appropriate to use the exception's constructor to build the exception. An example is a global exception class such as ArgumentException.

Restore state when methods don't complete due to exceptions

Callers should be able to assume that there are no side effects when an exception is thrown from a method. For example, if you have code that transfers money by withdrawing from one account and depositing in another account, and an exception is thrown while executing the deposit, you don't want the withdrawal to remain in effect.

public void TransferFunds(Account from, Account to, decimal amount){ from.Withdrawal(amount); // If the deposit fails, the withdrawal shouldn't remain in effect. to.Deposit(amount);}
Public Sub TransferFunds(from As Account, [to] As Account, amount As Decimal) from.Withdrawal(amount) ' If the deposit fails, the withdrawal shouldn't remain in effect. [to].Deposit(amount)End Sub

The preceding method doesn't directly throw any exceptions. However, you must write the method so that the withdrawal is reversed if the deposit operation fails.

One way to handle this situation is to catch any exceptions thrown by the deposit transaction and roll back the withdrawal.

private static void TransferFunds(Account from, Account to, decimal amount){ string withdrawalTrxID = from.Withdrawal(amount); try { to.Deposit(amount); } catch { from.RollbackTransaction(withdrawalTrxID); throw; }}
Private Shared Sub TransferFunds(from As Account, [to] As Account, amount As Decimal) Dim withdrawalTrxID As String = from.Withdrawal(amount) Try [to].Deposit(amount) Catch from.RollbackTransaction(withdrawalTrxID) Throw End TryEnd Sub

This example illustrates the use of throw to rethrow the original exception, making it easier for callers to see the real cause of the problem without having to examine the InnerException property. An alternative is to throw a new exception and include the original exception as the inner exception.

catch (Exception ex){ from.RollbackTransaction(withdrawalTrxID); throw new TransferFundsException("Withdrawal failed.", innerException: ex) { From = from, To = to, Amount = amount };}
Catch ex As Exception from.RollbackTransaction(withdrawalTrxID) Throw New TransferFundsException("Withdrawal failed.", innerException:=ex) With { .From = from, .[To] = [to], .Amount = amount }End Try

Capture exceptions to rethrow later

To capture an exception and preserve its callstack to be able to rethrow it later, use the System.Runtime.ExceptionServices.ExceptionDispatchInfo class. This class provides the following methods and properties (among others):

  • Use ExceptionDispatchInfo.Capture(Exception) to capture an exception and call stack.
  • Use ExceptionDispatchInfo.Throw() to restore the state that was saved when the exception was captured and rethrow the captured exception.
  • Use the ExceptionDispatchInfo.SourceException property to inspect the captured exception.

The following example shows how the ExceptionDispatchInfo class can be used, and what the output might look like.

ExceptionDispatchInfo? edi = null;try{ var txt = File.ReadAllText(@"C:\temp\file.txt");}catch (FileNotFoundException e){ edi = ExceptionDispatchInfo.Capture(e);}// ...Console.WriteLine("I was here.");if (edi is not null) edi.Throw();

If the file in the example code doesn't exist, the following output is produced:

I was here.Unhandled exception. System.IO.FileNotFoundException: Could not find file 'C:\temp\file.txt'.File name: 'C:\temp\file.txt' at Microsoft.Win32.SafeHandles.SafeFileHandle.CreateFile(String fullPath, FileMode mode, FileAccess access, FileShare share, FileOptions options) at Microsoft.Win32.SafeHandles.SafeFileHandle.Open(String fullPath, FileMode mode, FileAccess access, FileShare share, FileOptions options, Int64 preallocationSize, Nullable`1 unixCreateMode) at System.IO.Strategies.OSFileStreamStrategy..ctor(String path, FileMode mode, FileAccess access, FileShare share, FileOptions options, Int64 preallocationSize, Nullable`1 unixCreateMode) at System.IO.Strategies.FileStreamHelpers.ChooseStrategyCore(String path, FileMode mode, FileAccess access, FileShare share, FileOptions options, Int64 preallocationSize, Nullable`1 unixCreateMode) at System.IO.StreamReader.ValidateArgsAndOpenPath(String path, Encoding encoding, Int32 bufferSize) at System.IO.File.ReadAllText(String path, Encoding encoding) at Example.ProcessFile.Main() in C:\repos\ConsoleApp1\Program.cs:line 12--- End of stack trace from previous location --- at Example.ProcessFile.Main() in C:\repos\ConsoleApp1\Program.cs:line 24

See also

  • Exceptions
Best Practices for exceptions - .NET (2024)
Top Articles
Latest Posts
Article information

Author: Stevie Stamm

Last Updated:

Views: 5625

Rating: 5 / 5 (80 voted)

Reviews: 87% of readers found this page helpful

Author information

Name: Stevie Stamm

Birthday: 1996-06-22

Address: Apt. 419 4200 Sipes Estate, East Delmerview, WY 05617

Phone: +342332224300

Job: Future Advertising Analyst

Hobby: Leather crafting, Puzzles, Leather crafting, scrapbook, Urban exploration, Cabaret, Skateboarding

Introduction: My name is Stevie Stamm, I am a colorful, sparkling, splendid, vast, open, hilarious, tender person who loves writing and wants to share my knowledge and understanding with you.