Hopefully the era of leprosy and corona is over for this time and it’s time to get back to blogging. Exceptions are powerful feature of object-oriented languages as far as they are used like they are thought to use – throw exception only when something really unexpected happens. This advice should be taken seriously – here’s why.
Back in days when I was young and beautyful (now I’m only beautyful) I found utility application to get documents and their metadata out from SharePoint 2001. Although servers were powerful the exporting process was very slow. I took source code of utility and added bunch of sanity checks before exception handling to make sure that code doesn’t hit try-catch blocks if it can be avoided. Instead of 10 days our exports took 4.5 days after my little tweaks.
It’s hard to notice the effect of exceptions if we measure just one exception but they can be fatal to performance when they appear in loops. Let’s dig a little bit deeper.
How exceptions are handled?
Those who think exceptions are just fancy way to return errors are usually surprised when they hear how complex can things be internally – somewhere deep in Common Language Runtime (CLR).
The excellent book Expert .NET 2.0 IL Assembler by Serge Lidin describes what goes on under the hood.
The execution engine of the CLR processes an exception in two passes. The first pass determines which, if any, of the managed handlers will process the exception. Starting at the top of the Exception Handling (EH) table for the current method frame, the execution engine compares the address where the exception occured to the TryOffset and TryLength entries of each EH clause. If it finds that the exception happened in guarded block, the execution engine checks to see whether the handler specified in this clause will process the exception. … If none of the clauses in the EH table for the current method is suited to handling the exception, the execution engine steps up the call stack and starts checking the exception against EH tables of the method that called the method where the exception occured.
…
During the second pass, the finally and fault handlers are invoked with an empty evaluation stack. These handlers do nothing about the exception itself and work only with method arguments and local variables, so the execution engine doesn’t bother providing the exception object.
Without any numbers there’s are two alert reds for me:
- Processing in two phases
- Climbing up in method call stack and checking for exception handlers
These two activities both take probably more time than “usual” things we are doing in code.
Exception versus avoiding exception
Let’s get into code and numbers to get better understanding about exceptions effect to performance. I’m using simple program that fills list with some strings and nulls. After this let’s ask string length for each element in list and measure how long it takes.
Let’s start with version where asking string length is in try-catch block.
var list = new List<string>();
for (int i = 0; i < Math.Pow(10, 6); i++)
{
if (i == 1 || 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(watch.Elapsed);
On my machine this code ran through with 4.58 seconds.
Let’s remove exception handling and replace it with null check so we don’t ask length of null-string.
var list = new List<string>();
for (int i = 0; i < Math.Pow(10, 6); i++)
{
if (i == 1 || i % 2 != 0)
{
list.Add(i.ToString());
}
else
{
list.Add(null);
}
}
var watch = new Stopwatch();
watch.Start();
foreach(var s in list)
{
if(s == null)
{
continue;
}
var i = s.Length;
}
watch.Stop();
Console.WriteLine(watch.Elapsed);
After this modification the code takes 0.008 seconds to run. It’s roughly taken 570 times faster than letting code fall to exception. So, cost of exceptions can be very high.
Wrapping up
Exceptions are powerful feature and without exceptions we should think about our own mechanism how to organize error handling in our code. Like all other powerful features exceptions come with cost. This blog post demonstrated one aspect of it and one popular way how exceptions are abused. The code samples here went from 4.5 seconds to 0.008 seconds by avoiding exceptions. For long processes the win in time can be way bigger.
View Comments (14)
Your samples demonstrate that an explicit safety check is far more performant than a code failing to an exception...
Unfortunately, sometimes safety checks are insufficient due to lack of knowledge about possible exceptions.
A further comparison would be valuable: how much is the cost of the try/catch block even with null check in place? This would help to evaluate how convenient is to write try/catch blocks.
Good article. I tried with Benchmark.NET and cost of try catch block is 400% more than a simple null check. And ofcourse we cannot have all possible exceptions sometimes. But still it matters 👏🏻
Personal anecdotal rationale for why exceptions are horrible: they use reflection to establish their context when working out things like the callstack.
I don't know a great way to confirm this, but there are a variety of properties of System.Exception that imply some introspection of the throwing class has taken place (I believe the methodinfo is available from memory).
As I say, anecdotal, but a major reason I avoid them where possible.
Never pass or return null (use Option type if you need to represent 'None') and use monad results instead of exceptions for error handling and you will be a winner in life (and even better use programming languages that do no allow variables to be assigned to null.)
What if exceptions are handled using Exception middleware in asp.net core?
Agreed. I always try to avoid situations where an exception may occur, to take it a step further I believe if i have to use an exception handler I have already written bad code. Dart's nnbd is quite a step in the right direction and i wish more languages start doing it.
Maulik, ASP.NET Core exception middleware doesn't solve anything if we are talking about performance. The price of throwing and catching exception will remain the same.
I think the main point here, and I see it done quite often, is don't use exceptions to test conditions. Quite often I see people write code that is *intended* to throw an exception on every pass through a method. I pretty much only use exceptions when something really has gone wrong, and I expect to return all the way up the callstack.
I get that this defeats the purpose of a try/catch. But there is still use cases for it, just not when it's intended that your exception handling should be used every single pass.
Its worth reading. An eye opener of "when to use exception handler and when not to use it ". Thanks. Keep post things like this.
Thanks for an interesting and thoughtful post. Like most things in writing code, throwing exceptions should be a matter of making a balanced judgement.
Your code took a little over 4.5 seconds to throw half a million exceptions (about 0.00001 of a second each). There's a trade off here. If I'm writing code that's going to handle a very high volume of transactions I'll pay careful attention to this, because performance is likely to be a key factor. In back end code I'd have it front of mind all the time.
If I'm writing code where performance isn't an issue (business logic in a WPF app that a user is interacting with, for example) then performance at this level probably isn't a top priority. That doesn't mean I don't have an eye on it and don't put in guards to prevent exceptions. It means I preference making my code well structured and readable code over saving a few milliseconds execution time.
On .NET5 i get similar results as in this post.
But on (old) .NET Framework (4.8) try/catch runs slighly faster as version with if.
0.002 sec for try catch and 0.003sec for if
Why so?
What a nice article author.Thank you. Keep it up.
A common scenario for throwing exceptions is withing methods as they validate arguments. Exceptions are the recommended approach for this. I will say that I do not encourage throwing exceptions simply because some logic isn't prepared to handle some state though.
Tolerating invalid state is a big problem in many systems, if the invalid state is tolerated then it can be propagated and that can - in time - lead to a codebase that is a mess.
The question to ask with your example is this - are null entries in the list valid or not? If a null in the list is not expected or designed to happen then its a bug and the exception is therefore only a cost in the presence of an unfixed bug and should be thrown. But if nulls are regarded as legitimate, valid state, then I agree the code should ignore them but the the code should never have thrown an exception because that terminates processing and why would we terminate processing with valid data?
The old "fail early" advice comes up here, if there are conditions that arguments MUST comply with and they don't then we SHOULD throw and exception, this is a superb way to minimize the possibility that it gets tolerated. Invalid state should be weeded out and reported with urgency - IMHO.
So if a NULL should never appear in the list, your example where we skip over it, masks that fact and a bug will go unreported perhaps to only surface later in potentially dire circumstances.
Not sure this is a good article.
Ignoring the fact branch programming isn't a typical or realistic use case for exception handling, this still doesn't demonstrate that exception handling is taking more time. For that you should be looking at the underlying method calls and the percent of execution time each uses.
Are you running using a debug profile? If so the string creation in the catch block might be taking the extra time, not the exception handling. Maybe creating a new string in the code without the try-catch will yield a similar result? I wouldn't doubt it - string operations are expensive. But the only way to know would be to do an actual call time analysis. Showing a breakdown of method calls and the percent of time those calls took out of the entire execution time would actually tell us if it's the string creation or exception handing that's taking the time.