Avoid Writing Exception, do this instead in C#.
We all love the way C# provides us to handle exceptional scenarios using Exceptions. This could be handling null or missing arguments or invalid operations etc. We simply put our code in try-catch-finally
block and log the message accordingly. However, misuse of anything good can be more damaging than beneficial. In this article, we will prove that exceptions are costly and explore possible work around of it.
Note: You can try examples shown in this articles using your favorite online C# compiler like ShartLab.io.
What are Exceptions? (briefly)
It’s a feature provided by C# language to deal with any unexpected or exceptional situations that we may encounter during the execution of our program. We wrap our suspected code around try-catch-finally
block to leverage it’s benefit. C# not only handles exceptions, but it also performs clean-up afterwards in finally
block. Remember, that exception can occur in may places, in your code, in CLR or any third party library we are using.
Why Exceptions are expensive?
When C# finds an exceptions, it performs following steps:
- Exception Object Creation: The first step is to create an object that should be able to hold exception details. This includes error message, the stack trace, and the type of exception (e.g.
NullReferenceException
,IndexOutOfRangeException
etc.) - Stack Unwinding: This is where the costly process happens. Runtime begins a process called stack unwinding, where it searches for a try-catch block to handle the exception. It begins from the point where the exception occurred and moves up the call stack/ chain.
- Handling the Exception: Here we have two scenarios:
–Try-catch
exist: In this case, the control is transferred to the corresponding catch block. The code inside the catch block is executed, effectively handling the exception. Iffinally
block is mentioned then it will be executed to free-up the resources.
–Try-catch
doesn’t exist: In this case the exception is considered asunhandled
. The runtime continues unwinding the stack until it reaches the entry point of the application. (e.g.main
method in console application). After reaching the entry point, it can terminate the application, can show dialog box (for desktop app) or can generate HTTP 500 error in case of ASP.NET application.
– Note that, withouttry-catch-finally,
we may have resources being marked as busy while they aren’t in reality. Though they will be freed whengarbage collector
runs it’s cycle, GC doesn’t cover all the scenarios. - Global Exception Handling: If we have configured global handler then it will be invoked to log the error or perform other actions.
As you can see from above points, exception does lots of chores when they are called. This can impact performance of our application significantly if we are throwing exception for non-exceptional scenarios.
Let’s see by an example. Consider below code:
using System;
using System.Collections.Generic;
using System.Diagnostics;
var list = new List<string>();
for (int i = 1; i < 11; i++)
{
if (i % 2 != 0)
{
list.Add(i.ToString());
}
else
{
list.Add(null);
}
}
var watch = new Stopwatch();
watch.Start();
foreach (var s in list)
{
try
{
var i = s.Length;
}
catch (Exception ex)
{
var e = ex.Message;
}
}
watch.Stop();
Console.WriteLine($"Elapsed time with Exception: {watch.ElapsedTicks:N0} ticks");
watch.Reset();
watch.Start();
foreach (var s in list)
{
if(s == null)
{
continue;
}
}
watch.Stop();
Console.WriteLine($"Elapsed time without Exception: {watch.ElapsedTicks:N0} ticks");
Here, we are simply populating a List<string>
object with null and odd numbers from 1 to 10. So, it will have items like 1, null, 3, null, 5, null, 7, null, 9, null
. After populating list with those values, we are iterating and catching the exception for null values. The other iteration, doesn’t throw/catch exception, but have handled the null scenario gracefully.
Output:
Elapsed time with Exception: 2,494 ticks
Elapsed time without Exception: 67 ticks
The output clearly indicates the cost of Exception.
How to avoid Exceptions?
Now that we have learned that exceptions are costly, let’s look at several alternatives. However, even before that, let me add that I am not suggesting to completely get rid of Exceptions. They add value, however, we should be mindful in using them and should be used when we have truly an exceptional case.
There are two patterns applicable in this case.
- Tester-Doer Pattern
This pattern suggest to perform check for certain cases (tester) and then perform the operation (doer). For example, in our previous demonstration, we have a code like:
foreach (var s in list)
{
try
{
var i = s.Length;
}
catch (Exception ex)
{
var e = ex.Message;
}
}
We can update this as:
foreach (var s in list)
{
if(s == null)
{
continue;
}
}
if (s == null)
is nothing but a tester and continue
is a doer. Another example would be:
ICollection<int> numbers = ...
...
if (!numbers.IsReadOnly)
{
numbers.Add(1);
}
Here, number is a ReadOnly, so any addition will throw an exception. To overcome, we added a tester if (!numbers.IsReadOnly)
and doer numbers.Add(1);
.
2. Try-Parse Pattern
This pattern suggests that we should implement method with proper semantics to suggest the outcome. For example, DateTime
class has Parse
and TryParse
method. Parse
will throw an exception when an invalid string is provided, while TryParse
will return Boolean
indicating whether the string is valid for conversion or not. Note that, try-parse suggest to have an alternative version which throws exception.
Another approach would be to create a custom error class and return an instance of that class with error details to the main application. This is primarily useful in case of API response scenario.
Conclusion
While Exception are helpful, throwing them too frequently or cases where we can validate before performing an action, can hamper the performance of the application. The alternatives suggested in this approach can benefit greatly to address the need of an alternative.