Exception handling has been used in programming languages for decades to handle run-time errors in applications. However, throwing exceptions is costly in terms of performance, so we should avoid them in our code. This article discusses a few strategies we can use to avoid exceptions in C#.

To use the code examples provided in this article, you should have Visual Studio 2022 installed in your system. If you don’t already have a copy, you can download Visual Studio 2022 here.

Create a .NET Core console application project in Visual Studio

First off, let’s create a .NET Core console application project in Visual Studio. Assuming Visual Studio 2022 is installed in your system, follow the steps outlined below to create a new .NET Core console application project.

  1. Launch the Visual Studio IDE.
  2. Click on “Create new project.”
  3. In the “Create new project” window, select “Console App (.NET Core)” from the list of templates displayed.
  4. Click Next.
  5. In the “Configure your new project” window, specify the name and location for the new project.
  6. Click Next.
  7. In the “Additional information” window shown next, choose “.NET 8.0” as the framework version you would like to use.
  8. Click Create.

We’ll use this .NET Core console application project to examine several ways we can avoid exceptions in the subsequent sections of this article.

Why should we avoid exceptions?

In an application, throwing, re-throwing, and catching exceptions is a costly affair in terms of application performance because of the high processing overhead. Moreover, if you overuse exceptions in your application’s source code, it will make your source code more difficult to read and maintain.

When an exception is thrown in .NET, the normal running of your application is interrupted by a three-step process the runtime uses to handle the exception. Here is what happens:

  1. An exception object is created
    The runtime creates an exception object that contains the details of the exception such as the stack trace, error message, and the type of the exception that has been thrown (iArithmeticException, DivideByZeroException, IndexOutOfRangeException, StackOverflowException, etc.).
  2. The stack is unwound
    In this phase, the runtime searches for a matching try-catch block that can handle the exception that has occurred. This process starts from the point where the exception has been thrown and moves up the call stack until the runtime determines the right exception-handling code.
  3. The exception is handled
    If the runtime finds the right code to handle the exception, the source code inside the try-catch block will be executed. If the try-catch block contains a finally block, it will be executed after the try-catch block has executed. If the right exception-handling code is not available, the exception is considered to be unhandled. In this case, the control is propagated up the call stack until it reaches the entry point of the application. At this point the runtime terminates the application and displays the appropriate error message.

As you can see, handling exceptions involves a large amount of overhead and could significantly impact the performance of your application. Let’s us now examine some handy strategies we can use to avoid exceptions in C#.

Avoid exceptions using the Result pattern

The Result pattern is a good general strategy we can use to avoid exception handling code in your applications. You can implement this pattern in your C# code using a generic class that encapsulates the outcome (i.e., success or failure) of a particular operation. When an error has occurred in your application, you can use this pattern to return a result object in lieu of throwing an exception. This helps write code that is simple, clean, and easy to maintain.

Consider the following class named Result that represents the result of an operation.


public class Result
{
    public bool IsSuccess { get; }
    public T Value { get; }
    public string ErrorMessage { get; }
    private Result(T value, string errorMessage, bool isSuccess)
    {
        Value = value;
        ErrorMessage = errorMessage;
        IsSuccess = isSuccess;
    }
    public static Result Success(T value) =>
    new Result(value, null, true);
    public static Result Failure(string errorMessage) =>
    new Result(default(T), errorMessage, false);
}

The Result class contains two methods, namely Success and Failure. While the former returns a value, the latter returns an error message.

The following method shows how you can use the Result class to avoid exceptions in your code.


public Result DivideNumbers(int x, int y)
    {
        if (x == 0)
        {
            return Result.Failure("Division by zero is not allowed.");
        }
        int result = x / y;
        return Result.Success(result);
    }

The following code snippet shows how you might use the Result pattern with the DivideNumbers method.


var result = Test.DivideNumbers(15, 5);
if (result.IsSuccess)
    Console.WriteLine($"The result is: {result.Value}");
else
    Console.WriteLine($"Error occurred: {result.ErrorMessage}");

Avoid exceptions using the Try-Parse pattern

The Try-Parse pattern is another great way to avoid exceptions in your application. In C#, the Try-Parse pattern is represented using the TryParse method, which converts a data type into another and returns a Boolean value. If the parsing process succeeds, then the output is true, false otherwise. You can take advantage of this pattern to avoid exceptions in your code while converting data types as shown in the code snippet given below.


String str = "1000";
Boolean result = Int32.TryParse(str, out int n);
if (result == true)
    Console.WriteLine($"{n}");
else
    Console.WriteLine("Error in conversion");

Avoid exceptions by calling Try* methods

When converting a data type to another, you should take advantage of the Try-Parse pattern as shown above. Further, note that there are other Try methods such as TryGetValue. These methods return false if unsuccessful and return the result of a successful operation using an out parameter. The following code snippet shows how this can be accomplished.


int myValue;
if (dictionary.TryGetValue("MyKey", out myValue))
{
    //TryGetValue is successful so you can proceed as usual
}
else
{
    //TryGetValue is unsuccessful so display
    //or return an appropriate error message
}

Avoid exceptions by handling common conditions

You can also avoid exceptions by handling conditions that might trigger an exception at runtime. For example, you should check if a connection object is null or already closed before closing a database connection. This technique is shown in the code snippet given below.


if (connection!= null && connection.State != ConnectionState.Closed)
{
    connection.Close();
}

If you don’t check your database connection instances for null, or if you explicitly close an already-closed connection using a call to the Close method, you might encounter an InvalidOperationException. The following code snippet shows how you should handle InvalidOperationException in your code.


try
{
    connection.Close();
}
catch (InvalidOperationException ex)
{
    Console.WriteLine(ex.Message);
}

Excess usage of exceptions can degrade your application’s performance. Therefore you should take measures to avoid handling exceptions in your code when they can easily be replaced by logic. A rule of thumb is to check for common error conditions in your code so that you can avoid exceptions. In all other cases, you can take advantage of the Result pattern that provides a structured method for handling errors.

Finally, remember to use exceptions only in exceptional cases, i.e., where you already know an error might occur. Never use exceptions to manage control flow in an application.